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

名称类型必填默认值说明
openboolean✓—是否打开弹窗
titlestring—标题文本;或用 #title 插槽完全覆盖
descriptionstring—描述文本;当没有 default 插槽时才渲染
confirmTextstring—确认按钮文字;提供此 prop 时才显示底部操作栏
cancelTextstring'取消'取消按钮文字,'' 则隐藏取消按钮
closablebooleantrue是否显示关闭 ✕;传 false 且无 title 时为无 header/✕ 的裸内容弹窗
dismissiblebooleantrue是否允许点遮罩 / Esc 关闭;传 false 锁死(仅 ✕ / 按钮 / 业务代码可关,如广告倒计时)

Events

事件名载荷类型说明
update:openvalue: boolean打开状态变更时触发
confirmvoid点击确认按钮时触发
cancelvoid点击取消按钮时触发

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 关掉整套。