Skip to content

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全部正文;空间不足时只有它滚动,标题与操作栏固定
iconbasic标题上方、水平居中;优先于 icon 属性
footer-leftbasic底部左端,第一行
footer-rightbasic底部右端;放不下时整组折到第二行
title-rightfullscreen顶部栏行尾,与标题同一行

标题本身由 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:openbooleanfalse是否显示
titlestring—标题,同时作为对话框的无障碍名称
variant"basic" | "fullscreen""basic"basic 居中卡片;fullscreen 铺满视口
iconIconifyIcon | string—标题上方的图标,仅 basic;存在时标题居中
dismissiblebooleantrue是否允许点击遮罩 / Escape 关闭
close-labelstring"Close"全屏变体关闭按钮的 aria-label

插槽 ​

名称变体说明
default全部正文,独立滚动
iconbasic标题上方的自定义图标,优先于 icon
footer-leftbasic底部左端的次要操作
footer-rightbasic底部右端的主操作;放不下时整组折行
title-rightfullscreen顶部栏行尾的操作

可访问性 ​

  • 原生 <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 用卡片默认内边距铺满视口。
  • 进出场:basic 200ms 淡入并从 95% 放大,退出 150ms;fullscreen 250ms 从底部滑入,200ms 滑出。
  • class 与 style 透传到面板,可以覆盖外观,但不要改 position 与 inset;也不要再改宽度, 宽度由变体与视口共同决定。