Dialog 对话框
Dialog 打断当前流程、要求用户做决定或填写少量信息。组件只有一个 <dialog> 宿主和一个面板, 面板本体是 Card:圆角、表面色、标题行与底部操作栏都沿用卡片的规则, Dialog 只负责模态语义、进出场动画和插槽到卡片的映射。
basic(默认)—— 视口居中的卡片,最大宽度 560px,用于确认、提示、简短表单。fullscreen—— 铺满视口,顶部栏固定、正文独立滚动,用于需要完整空间的编辑流程。
v-model:open 是唯一的显示状态来源。组件 Teleport 到 body,用原生 <dialog> + showModal() 拿到置顶图层、焦点陷阱和「背景不可交互」;打开期间同时锁定 body 滚动。 关闭由底部操作按钮、全屏变体的关闭按钮、遮罩点击与 Escape 触发,后两者受 dismissible 控制。
最小用法
title 给标题,默认插槽是正文,底部操作放在 footer-right。
vue
<script setup lang="ts">
import { ref } from "vue";
import { Button, Dialog } from "@sharpdotnut/dotsharp";
const open = ref(false);
</script>
<template>
<Button @click="open = true">打开对话框</Button>
<Dialog v-model:open="open" title="基础对话框标题">
对话框是浮在页面内容上方的模态窗口,用来传达关键信息,或要求用户做出选择。
<template #footer-right>
<Button variant="text" @click="open = false">取消</Button>
<Button @click="open = false">接受</Button>
</template>
</Dialog>
</template>插槽落在面板的哪里
| 插槽 | 变体 | 位置 |
|---|---|---|
default | 全部 | 正文;空间不足时只有它滚动,标题与操作栏固定 |
icon | basic | 标题上方、水平居中;优先于 icon 属性 |
footer-left | basic | 底部左端,第一行 |
footer-right | basic | 底部右端;放不下时整组折到第二行 |
title-right | fullscreen | 顶部栏行尾,与标题同一行 |
标题本身由 title 属性提供:basic 渲染 24px 标题,fullscreen 渲染 20px 并在左侧带关闭按钮 (关闭按钮占用卡片的 title-left 槽位)。basic 不渲染关闭按钮——它靠底部操作和遮罩/Escape 退出。
带图标与居中标题
icon 与 Icon 的 icon 同源,渲染在标题上方,同时把标题改为居中;正文仍然左对齐。 需要加载指示器、头像这类自定义内容时用 icon 插槽,它优先于 icon 属性。
vue
<script setup lang="ts">
import { ref } from "vue";
import { Button, Dialog } from "@sharpdotnut/dotsharp";
import progressActivity from "@iconify-icons/material-symbols/progress-activity";
const open = ref(false);
</script>
<template>
<Dialog v-model:open="open" title="重置设置?" :icon="progressActivity">
正文……
<template #icon>
<!-- 自定义内容,例如 <Icon :icon="progressActivity" class="animate-spin" /> -->
</template>
<template #footer-right>
<Button variant="text" @click="open = false">取消</Button>
<Button variant="text" @click="open = false">接受</Button>
</template>
</Dialog>
</template>底部操作:主操作在右
footer-right 是右对齐的一组操作:主操作(默认按钮)放末尾,次要操作用 variant="text" 压低视觉重量。
vue
<Dialog v-model:open="open" title="删除这个文件夹?">
删除后「设计稿-2026」里的 32 个文件会一并移除,且无法从回收站恢复。
<template #footer-right>
<Button variant="text" @click="open = false">取消</Button>
<Button @click="open = false">删除</Button>
</template>
</Dialog>底部操作:左右分组
需要「了解更多」这类和决定无关的入口时,把它放进 footer-left,不要靠 class="mr-auto" 之类的技巧排版。
选择结果:未决定
vue
<Dialog v-model:open="open" title="启用位置服务?">
允许此服务帮助其他应用确定你的位置。这会共享匿名位置数据,即使没有应用在运行。
<template #footer-left>
<Button variant="text">了解更多</Button>
</template>
<template #footer-right>
<Button variant="text" @click="open = false">不同意</Button>
<Button variant="text" @click="open = false">同意</Button>
</template>
</Dialog>放不下时折成两行
底部是 flex-wrap 容器:两组操作都放得下时同一行左右分开;放不下时右组整组折到第二行并保持右对齐, 左组留在第一行左端。面板最宽 560px(视口更窄时再减去 48px 内边距),所以按钮一多就会看到折行。
vue
<Dialog v-model:open="open" title="权限请求">
正文……
<template #footer-left>
<Button variant="text">了解更多</Button>
</template>
<template #footer-right>
<Button variant="text">暂不开启</Button>
<Button variant="text">稍后提醒我</Button>
<Button variant="text">仅此次允许</Button>
<Button variant="text">始终允许</Button>
</template>
</Dialog>全屏对话框
variant="fullscreen" 铺满视口:顶部栏固定(关闭按钮 + 标题 + title-right),正文在剩余空间内独立滚动, 进入、退出都从底部滑入滑出。适合分步表单、边看边改的编辑页。
vue
<script setup lang="ts">
import { ref } from "vue";
import { Button, Dialog, Input } from "@sharpdotnut/dotsharp";
const open = ref(false);
const eventName = ref("");
const fromDate = ref("");
const fromTime = ref("");
</script>
<template>
<Dialog v-model:open="open" variant="fullscreen" title="全屏对话框标题">
<div class="mx-auto flex max-w-xl flex-col gap-4">
<Input v-model="eventName" label="事件名称" />
<div class="grid grid-cols-2 gap-4">
<Input v-model="fromDate" label="开始日期" />
<Input v-model="fromTime" label="开始时间" />
</div>
</div>
<template #title-right>
<Button variant="text" @click="open = false">保存</Button>
</template>
</Dialog>
</template>关闭按钮固定使用 close 图标,无障碍名称由 close-label 提供(默认 Close)。
关闭行为
dismissible 默认为 true:点击遮罩、按 Escape 都会关闭。置为 false 后两种方式都失效, 对话框只能由操作按钮关闭——用于必须选择、或存在未保存修改的场景。
Escape 走原生 cancel 事件,组件始终拦截它:先播完面板的离场动画,再真正 close() 原生对话框, 所以动画不会被截断。
vue
<Dialog v-model:open="open" title="保存更改?" :dismissible="false">
你有未保存的修改,请选择一个操作继续。
<template #footer-right>
<Button variant="text" @click="open = false">放弃</Button>
<Button variant="text" @click="open = false">保存</Button>
</template>
</Dialog>长内容
正文区域受面板最大高度约束(100dvh - 3rem),超出时在正文里滚动,标题与底部操作栏保持固定。 底部操作栏不会随之滚走,因此不需要把按钮放进正文。
vue
<Dialog v-model:open="open" title="服务条款">
<p v-for="index in 30" :key="index" class="mb-3">第 {{ index }} 条……</p>
<template #footer-right>
<Button variant="text" @click="open = false">关闭</Button>
</template>
</Dialog>无标题对话框
没有 title 就没有 aria-labelledby,此时必须在组件上直接传 aria-label——它会落到 <dialog> 上, 读屏仍然能读出对话框的名字。同时标题行不会渲染,正文贴着面板顶部。
vue
<Dialog v-model:open="open" aria-label="快捷操作">
<div class="flex flex-col gap-2">
<Button variant="text">复制</Button>
<Button variant="text">移动到…</Button>
<Button variant="text">删除</Button>
</div>
</Dialog>API
Dialog
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
v-model:open | boolean | false | 是否显示 |
title | string | — | 标题,同时作为对话框的无障碍名称 |
variant | "basic" | "fullscreen" | "basic" | basic 居中卡片;fullscreen 铺满视口 |
icon | IconifyIcon | string | — | 标题上方的图标,仅 basic;存在时标题居中 |
dismissible | boolean | true | 是否允许点击遮罩 / Escape 关闭 |
close-label | string | "Close" | 全屏变体关闭按钮的 aria-label |
插槽
| 名称 | 变体 | 说明 |
|---|---|---|
default | 全部 | 正文,独立滚动 |
icon | basic | 标题上方的自定义图标,优先于 icon |
footer-left | basic | 底部左端的次要操作 |
footer-right | basic | 底部右端的主操作;放不下时整组折行 |
title-right | fullscreen | 顶部栏行尾的操作 |
可访问性
- 原生
<dialog>+showModal()负责模态语义:内容进入浏览器置顶图层,背景不可交互,焦点被限制在对话框内, 关闭后自动回到触发元素;组件不自己实现焦点陷阱。 - 有
title时通过aria-labelledby关联标题;没有title时请自己传aria-label。 aria-*、事件等未列出的属性透传到<dialog>;只有class与style属于内部面板。- Escape 触发原生
cancel,受dismissible控制;遮罩点击只在basic变体生效。 - 打开期间锁定
body滚动;多个对话框叠加时,只在最后一个关闭后恢复。
结构
- Teleport 到
body,输出一个常驻的<dialog>宿主(m-0 h-full max-h-none w-full max-w-none border-0 bg-transparent p-0, 类名ds-dialog__root),面板在它内部绝对定位。 - 遮罩是原生
::backdrop:basic用backdrop:bg-ds-scrim(--ds-scrim,32% 黑),fullscreen透明。 - 面板是
Card(variant="surface"、level="1",类名ds-dialog):basic用padded="false"加 24px 内边距, 最大宽度 560px、rounded-ds-xl、shadow-ds-3;fullscreen用卡片默认内边距铺满视口。 - 进出场:
basic200ms 淡入并从 95% 放大,退出 150ms;fullscreen250ms 从底部滑入,200ms 滑出。 class与style透传到面板,可以覆盖外观,但不要改position与inset;也不要再改宽度, 宽度由变体与视口共同决定。