vue-popup-ctrl

vue 弹窗控制组件
196
Install
npm install vue-popup-ctrl
Latest Version:1.0.0
Vue:3
License:MIT
Last Updated:Oct 1, 2026
Links: GitHub  ·  npm
Maintainer: aevwind

vue-popup-ctrl

Vue 3 弹窗与 toast 控制插件,支持传递 props、监听事件、遮罩、动画和显示频率限制。

安装与接入

需要 Vue 3.5 或更高的 3.x 版本。插件使用项目中的 Vue,不另外打包 Vue。

npm install vue-popup-ctrl

在应用入口安装插件,并手动注册业务弹窗。插件不会自动扫描组件目录。

通过 Vite、Webpack 等支持 CSS 导入的构建工具使用插件或 PopupCtrl 组件时,样式会自动引入,无需另外导入 vue-popup-ctrl/style.css。原来的手动导入方式仍然兼容。

// src/main.ts
import { createApp } from 'vue'
import PopupPlugin, { type PopupConfig } from 'vue-popup-ctrl'
import App from './App.vue'
import PopupConfirm from './components/PopupConfirm.vue'

const app = createApp(App)
const options: PopupConfig = { maskClose: true }
app.component('PopupConfirm', PopupConfirm)
app.use(PopupPlugin, options)
app.mount('#app')

在应用根组件放置一个控制容器。内部已经使用 Teleport,无需再包一层。

<!-- src/App.vue -->
<template>
  <main>应用内容</main>
  <PopupCtrl :opacity="0.8" :bg-blur="false" />
</template>

默认导出是供 app.use() 使用的插件;具名导出 PopupCtrl 是控制容器组件。

创建与打开弹窗

业务弹窗通过 props 接收数据,通过 emit 触发回调:

<!-- src/components/PopupConfirm.vue -->
<script setup lang="ts">
defineProps<{ id: number; title?: string }>()
const emit = defineEmits<{
  confirm: [value: { accepted: boolean }]
  close: []
}>()

function confirm() {
  emit('confirm', { accepted: true })
  emit('close')
}
</script>

<template>
  <section class="dialog">
    <h2>{{ title || '确认操作' }}</h2>
    <p>当前编号:{{ id }}</p>
    <button @click="confirm">确认</button>
    <button @click="emit('close')">取消</button>
  </section>
</template>

<style scoped>
.dialog { padding: 24px; border-radius: 12px; background: white; }
</style>

在其他组件的 setup 中取得 store,在用户交互时打开弹窗:

<script setup lang="ts">
import { inject } from 'vue'
import type { PopupStore } from 'vue-popup-ctrl'

const popupStore = inject<PopupStore>('popupStore')
if (!popupStore) throw new Error('请先安装 PopupPlugin')
const store = popupStore

function openConfirm() {
  store.open('PopupConfirm', { id: 1, title: '确认提交' })
    .on('confirm', data => console.log(data.accepted))
}
</script>

<template>
  <button @click="openConfirm">打开弹窗</button>
</template>

普通模块也可以使用 usePopupStore()。所有 store 共享弹窗列表,但各自使用传入的默认配置:usePopupStore() 不会自动继承 app.use() 的配置。组件内需要这些配置时使用 inject('popupStore')。

默认配置在创建 store 时独立复制并冻结,之后修改原对象不会影响它。store.popupConfig 仅供读取;单次配置或弹窗配置仍可通过对应的 config() 方法设置。

import { usePopupStore } from 'vue-popup-ctrl'

const store = usePopupStore({ maskClose: true })
store.open('PopupConfirm', { id: 1 })

关闭与事件

const popup = store.open('PopupConfirm', { id: 1 })

popup.close()         // 请求关闭:触发 close 监听,可以被拦截
store.close(popup.id) // 强制关闭指定弹窗,不触发 close 监听
store.close()         // 强制关闭最后一个尚未关闭的弹窗

弹窗内部的 emit('close') 和允许关闭时的遮罩点击,也会触发 close 监听。没有额外监听时自动关闭;注册监听后,由回调决定何时调用第一个参数提供的关闭函数:

store.open('PopupConfirm', { id: 1 })
  .on('close', close => {
    // 可以先确认或保存数据;不调用 close() 则保留弹窗。
    close()
  })

关闭回调支持异步操作,保存完成后调用回调入参 close() 确认关闭。组件 emit('close') 和遮罩点击触发的异步异常由 Vue 错误处理机制接收,可在 app.config.errorHandler 中处理。手动 popup.close() 遇到异步监听时返回 Promise<void>,成功时等待所有异步监听完成,失败时拒绝;仅同步监听时仍返回 undefined。异步失败本身不会自动关闭弹窗。

const popup = store.open('PopupConfirm', { id: 1 })
  .on('close', async close => {
    await saveData()
    close()
  })

try {
  await popup.close()
} catch (error) {
  console.error('保存失败', error)
}

on(event, callback) 返回弹窗句柄,支持链式调用。使用同一个函数引用解绑,不影响其他回调或 Promise 监听:

const popup = store.open('PopupConfirm', { id: 1 })
const handleConfirm = (data: { accepted: boolean }) => console.log(data)
popup.on('confirm', handleConfirm)
popup.un('confirm', handleConfirm)

不传回调时返回 Promise,成功值为事件的第一个参数或 undefined。被频率限制的禁用句柄会立即返回 undefined,包括 on('close'),并且不会调用普通事件回调。Promise 完成或取消后会移除对应的内部监听,不会绕过其他 close 回调的拦截。事件触发前收到关闭请求时,Promise 会拒绝,拒绝值为关闭函数,需要处理取消分支:

const popup = store.open('PopupConfirm', { id: 1 })
try {
  const data = await popup.on('confirm')
  console.log(data)
} catch (close) {
  if (typeof close === 'function') close()
} finally {
  store.close(popup.id)
}

启用的句柄调用 popup.on('close') 时,成功值是关闭函数;本次关闭请求不会自动关闭弹窗,可检查 typeof close === 'function' 后调用它确认关闭。Promise 完成后,后续关闭请求由剩余的 close 回调或默认关闭规则处理。store.close() 绕过所有事件,因此不会完成等待事件的 Promise;需要取消处理时使用 popup.close()。

配置与方法

单次配置可作为第三个参数传入,或通过句柄修改;store.config() 与 store.props() 只对下一次创建生效。

创建弹窗时,props 按缓存数据、本次传入数据、名称 query 的顺序进行顶层合并,后者覆盖前者,undefined 不覆盖已有值。同名对象属性会整项替换,不进行深合并;嵌套对象、数组、响应式对象和 ref 保留原引用,支持循环引用。popup.props(data) 替换 props 时也会保留嵌套引用。

store.props(data) 注册时浅复制顶层并返回绑定这次缓存的链式视图。缓存被消费或被新的 props() 覆盖后,旧视图不再取得缓存,也不会消费新的数据。原 store 仍可消费当前缓存;需要类型推断时使用 store.props(data).open(...)。only、daily、once 和 bottom 不会修改传入的配置对象。

store.open('PopupConfirm', { id: 1 }, { opacity: 0, maskClose: false })
store.config({ anime: 'bottom' }).open('PopupConfirm', { id: 2 })

const popup = store.open('PopupConfirm', { id: 3 })
popup.props({ id: 4 }).config({ maskColor: '#112233', opacity: 0.3 })

PopupConfig 是类型,需要使用 import type。常用字段如下:

字段 类型 说明
only boolean 完整名称相同的弹窗只保留一个
anime 'bounce' | 'bottom' | 'none' | 'boom' | 'light' | 'confetti' 默认 bounce;none 关闭动画
opacity number 支持 0;未设置时使用容器配置
zIndex number 遮罩在页面中的层级
maskClose boolean 是否允许点击遮罩请求关闭
maskColor string 遮罩颜色
maskStyle Vue StyleValue 样式对象、CSS 字符串、数组,也可为 null 或 undefined
rootClassName string 遮罩容器附加类名
type 'daily' | 'once' | '' 显示频率,通常使用同名 store 方法设置

boom、light 和 confetti 保留了过渡样式,但当前包不包含爆炸、闪光图片或礼花粒子实现。confettiConf 是预留配置,不会播放粒子特效。

控制容器支持 bgBlur(默认 false)、maskClose(默认 false)、maskColor(默认 #000000)和 opacity(默认 0.8)。模板中使用 mask-close,不是 maskColose;数字应使用绑定,例如 :opacity="0.2"。背景模糊时,body 直属元素可加 dis_popup_blur 类避免被模糊。

遮罩固定覆盖视口,层级直接参与页面堆叠,maskStyle 中的 z-index 可覆盖 zIndex。开启背景模糊时,仅实际层级最高的遮罩保持清晰;同层级取 DOM 中靠后的遮罩,退场中的遮罩也参与比较。最后一个遮罩退场结束后才恢复页面滚动和模糊状态,卸载容器时立即恢复。

maskColor 支持 3、4、6、8 位 HEX 和逗号分隔的 RGB/RGBA,RGBA 小数 alpha 可正确解析;显式 opacity 会覆盖颜色自带的 alpha,包括 0。maskStyle 的背景样式仍作为内联样式覆盖遮罩背景。

store 方法 行为
open(name, props?, config?) 打开弹窗,返回句柄
bottom(name, props?, config?) 使用底部进入动画
only(name, props?, config?) 相同完整名称复用已有弹窗,不更新其数据
daily(name, props?, config?) 按当前浏览器本地日期限制显示
once(name, props?, config?) 按存储命名空间和完整名称限制显示
close(id?) 强制关闭指定弹窗,省略 id 则关闭最后一个
toast(text, config?) 显示提示,配置可为毫秒数或 { duration: 3000 }
props(data) 设置下一次创建的 props,返回绑定该缓存的视图
config(options) 设置下一次创建的配置

名称可附带 query,如 PopupNotice?code=a。参数作为 props 传给组件,并覆盖第二个参数中的同名字段;query 值保持字符串,无值或空值为 true,不会自动转数字或进行 URL 解码。需要数值、对象等类型时使用第二个参数。

only('PopupNotice?code=a') 与 only('PopupNotice?code=b') 是两个独立弹窗。去重及频率限制按完整字符串区分,不归一化 query 顺序。

daily / once 在收到弹窗的 close 事件时写入 localStorage 标记;store.close() 绕过事件,因此不会写入标记。emit('close', true) 会清除标记。命名空间优先取 sessionStorage.RELEASE,否则使用页面 origin。存储被禁用或写入失败时,弹窗仍可使用,但不能保证频率限制。被限制的句柄会设置 disabled: true,不会加入显示列表。

句柄还提供 id、组件名 name、完整名称 key、data、option、show 和 ref。ref 在组件挂载后才能取得,关闭或容器卸载后恢复为 undefined;script setup 组件需要通过 defineExpose 公开要调用的方法。toast 句柄支持 close() 提前关闭。

TypeScript 提示

包内包含 ESM 和 CommonJS 对应的声明,可用于 NodeNext 和 Bundler 解析模式。不声明业务弹窗类型也能调用 API。

如需 props、事件和组件实例提示,在项目中增加声明文件,并确保它被项目的 TypeScript 配置包含:

// src/popup.d.ts
import 'vue'

declare module 'vue' {
  interface GlobalComponents {
    PopupConfirm: typeof import('./components/PopupConfirm.vue')['default']
  }
}

export {}

类型推断读取 Vue 的 GlobalComponents 中以 Popup 加大写字母开头的名称。类型声明不能代替运行时的 app.component() 注册。

store.props(...).config(...).open(...) 保留组件 props、事件和实例类型检查。已注册组件的必填 props 必须由本次参数或已知缓存补齐,缓存也会在选择组件时校验。缓存视图可重复使用,因此仅依赖缓存的必填字段包含 undefined;根 store 没有由本次参数保证的字段为 unknown,使用前需检查。

名称中的字面量 query 会反映在创建后的 popup.data 类型中,输入 props 仍按组件声明校验。例如组件声明 id: number 时:

const popup = store.open('PopupConfirm?id=2&enabled', { id: 1 }, { only: false })
popup.data.id      // 类型为 '2'
popup.data.enabled // 类型为 true

const replaced = popup.props({ id: 3 })
replaced.data.id   // 类型恢复为 number,query 不会再次应用

替换 props 后使用返回的句柄读取新数据类型;已有变量的静态类型不会随对象修改而自动更新。

only() 或第三个参数可能启用 only 时,返回的 data 字段为 unknown,因为复用弹窗不会更新已有数据或再次应用 query。类型目前不追踪默认配置和 store.config() 缓存中的 only;需要保证本次创建的数据类型时,在第三个参数显式设置 only: false。

动态或过长的 query 使用保守类型:已知字段保留原类型以及可能的字符串或 true,未知字段为 unknown,使用前需检查实际值。

SSR

可以在没有 window、document 和 Web Storage 的环境中导入包及渲染控制容器。Node 入口不加载 CSS,客户端构建入口会自动关联样式;容器在客户端挂载后才创建 Teleport 和修改页面样式。打开弹窗、toast 等操作应在客户端执行;store 是模块级共享状态,不应保存服务端请求数据,也不提供请求级隔离。

如果直接通过浏览器 <script> 加载 dist/vue-popup-ctrl.umd.js,没有构建工具处理样式依赖,仍需使用 <link> 引入 dist/vue-popup-ctrl.css。

开发与发布

npm ci
npm test
npm pack

npm test 会检查源码类型、构建 JS/CSS 和声明,再运行行为与消费端类型回归。仓库的 prepack 会执行这套流程,所以从仓库运行 npm pack 或 npm publish 时都会重新验证和构建。不要通过 --ignore-scripts 跳过检查。参见 npm 生命周期文档。

发布包仅包含 dist、README、LICENSE 与 package.json。在独立 Vue 项目安装生成的 vue-popup-ctrl-1.0.0.tgz,验证组件、样式与类型后,再进行发布。版本变更后使用相应文件名。

发布前确认 npm 账号具有包名权限,且版本未发布;按 npm 提示完成身份验证。下面最后一条命令会真正上传公开包:

npm login --registry=https://registry.npmjs.org
npm whoami --registry=https://registry.npmjs.org
npm publish --registry=https://registry.npmjs.org

具体账号与发布要求参见 npm 公共包发布说明。项目不会自动登录或发布。

License

MIT,见 LICENSE。

Related Packages

vc-popup-base

vc-popup-base

1,618
vc-popup-picker

vc-popup-picker

1,241
@jspatrick/tooltip

A Vue Popup Plug

1,208
@performing/cookie-popup-gdpr

Simple and easy to use cookie popup for GDPR compliance

1,088 1
vc-popup-calendar

vc-popup-calendar

1,024

Version History

Version Released Vue License
1.0.0 ^3.5.0 MIT