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 | ✓ | — | 控制對話框顯示/隱藏,搭配 v-model:open 使用 |
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 | boolean | 對話框顯示狀態變化時觸發 |
confirm | void | 點擊確認按鈕時觸發 |
cancel | void | 點擊取消按鈕時觸發 |
插槽(Slots)
| 名稱 | 說明 |
|---|---|
default | 對話框主體內容區域 |
title | 自訂標題區域 |
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關掉整套。