SwModal
SwModal 基于 reka-ui Dialog 封装,通过 DialogPortal 将弹窗内容传送到 <body> 下渲染,避免 CSS 层叠上下文问题。组件提供标题、描述、主体内容和底部操作的完整结构,并支持通过插槽完全自定义各区域。
装配前置:SwModalHost
framework 同时提供 SwModalHost 容器组件,是命令式 useModal 渲染挂载点。app.vue 顶层放置一次即可:
<!-- app.vue -->
<template>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
<SwModalHost />
</template>
由
sw create生成的项目已内置,多数情况无需手动加。SwModalHost仅服务于useModal()命令式弹窗;声明式<SwModal>直接挂在组件树里不依赖此容器。
声明式用法
通过 v-model:open 绑定显示状态,配合 title、description、confirm-text prop 快速构建确认弹窗。
基础用法
自定义底部
通过 #footer 插槽可以完全替换默认的确认/取消按钮,实现多按钮布局、条件禁用等复杂交互场景。
自定义底部
命令式用法(useModal)
useModal().open() → Promise<boolean>
除了在模板里声明 <SwModal>,框架还提供命令式 API,在业务代码中直接 await 弹窗结果,适合确认删除、提示等场景。
第一步: app.vue 放置一次 <SwModalHost />(由 sw create 生成的项目已内置,无需手动添加):
<!-- app.vue -->
<template>
<NuxtLayout><NuxtPage /></NuxtLayout>
<SwModalHost />
<SwToast />
</template>
第二步: 在任意组件中调用 useModal().open():
<script setup lang="ts">
const modal = useModal()
async function handleDelete() {
const confirmed = await modal.open({
title: '确认删除',
description: '此操作不可撤销,确定继续吗?',
confirmText: '删除',
cancelText: '取消',
})
if (confirmed) await deleteItem()
}
// 只读提示(隐藏取消按钮)
async function handleAlert() {
await modal.open({
title: '温馨提示',
description: '活动已结束',
confirmText: '知道了',
cancelText: '',
})
}
</script>
open() 返回 Promise<boolean>:用户点确认 resolve true,点取消或关闭 resolve false。同一时刻只有一个命令式弹窗,多次调用会覆盖前一个。
SwModalHost是基础设施组件,不接受任何 props,也无需在模板中多处使用——放在app.vue一次即可覆盖全应用。
默认皮肤与 token 覆盖
SwModal 自带默认皮肤(framework 注入 modal.css):--sw-modal-* 变量默认映射语义 token,自动跟随明暗主题;业务在 overrides.css 平级 :root 重定义 --sw-modal-* 即可换皮(弹窗 Teleport 到 body,scoped 覆盖不到);整套皮肤可用 framework.styles.modal = false 关闭。
弹窗层级由 --sw-modal-z 控制(默认 1000,fw#332 z 层旋钮)——与页面内其它浮层撞层时改它,别去 :deep 覆盖。
皮肤 token 覆盖(圆角 / 宽度 / 品牌色)
几何:视口安全默认值 + per-dialog 定制
弹窗几何默认视口安全:width: min(--sw-modal-width(28rem), 100dvw - 2rem)、max-height: min(--sw-modal-max-height(85dvh), 100dvh - 2rem)——窄屏保留 1rem 边距,长内容自动「头尾钉住、体内滚动」(body 内部出纵向滚动条),不会溢出视口。
class / attrs 会透传到 .sw-modal-content(弹窗内容根元素),这是 per-dialog 定制的官方通道——在业务 class 里定义 --sw-modal-width / --sw-modal-max-height 即可单弹窗定宽高(:root 重定义则是全站所有弹窗):
<SwModal v-model:open="visible" class="wide-dialog" title="批量编辑">…</SwModal>
<style>
/* 非 scoped:content Teleport 到 body,scoped 属性到不了 */
.wide-dialog {
--sw-modal-width: 44rem;
}
</style>
内容超宽时 body 会出横向滚动条(有意保留的暴露信号,静默截断会藏 bug)——正解是经 class 通道给该弹窗定合适宽度,而不是在内容里硬撑
min-width。
关闭行为(dismissible / closable)
锁定弹窗 + 裸内容弹窗
Props
| 名称 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
open | boolean | ✓ | — | 是否打开弹窗 |
title | string | — | 标题文本;或用 #title 插槽完全覆盖 | |
description | string | — | 描述文本;当没有 default 插槽时才渲染 | |
confirmText | string | — | 确认按钮文字;提供此 prop 时才显示底部操作栏 | |
cancelText | string | '取消' | 取消按钮文字,'' 则隐藏取消按钮 | |
closable | boolean | true | 是否显示关闭 ✕;传 false 且无 title 时为无 header/✕ 的裸内容弹窗 | |
dismissible | boolean | true | 是否允许点遮罩 / Esc 关闭;传 false 锁死(仅 ✕ / 按钮 / 业务代码可关,如广告倒计时) |
Events
| 事件名 | 载荷类型 | 说明 |
|---|---|---|
update:open | value: boolean | 打开状态变更时触发 |
confirm | void | 点击确认按钮时触发 |
cancel | void | 点击取消按钮时触发 |
Slots
| 插槽名 | 说明 |
|---|---|
title | 自定义标题区域 |
default | 弹窗主体内容(覆盖 description) |
footer | 完全自定义底部操作栏(会覆盖默认的取消/确认按钮) |
注意事项
- 受控与命令式两套用法别在同一文件混用(Rule SM-001)——
<SwModal v-model:open="visible">与useModal().open({...})都改全局弹窗状态,同一组件内混用会导致状态错乱(一次操作弹两个 / 互相覆盖关闭)。二选一:局部弹窗用受控,全局确认框用命令式。 <SwModalHost />必须在app.vue根级挂一次(Rule SM-002)—— 没挂则useModal().open()什么都不显示;挂在深层路由组件则路由切换时被销毁、命令式弹窗失效。framework 有 runtime 兜底:client 端检测到未挂载会console.warn一次(用 useState 去重防刷屏),dev / prod 均生效。- 焦点管理已内建,别自建 overlay —— 基于 reka-ui Dialog,开弹层聚焦 / focus trap / Esc 关闭 / 关闭还焦全部自带(Rule UX-005)。自写
<div v-if="open">遮罩会让键盘与读屏用户无法操作。 - 它不是零样式组件 —— framework 默认注入
modal.css(毛玻璃遮罩 + 完整卡片视觉),applier 零自定义样式时就是可直接用的观感。别按「无样式弹窗」预期去从头写皮肤,先看上面默认皮肤与 token 覆盖:优先覆盖--sw-modal-*(无需:deep穿透),确需完全自绘才用framework.styles.modal = false关掉整套。