开发预览 更新于 2026-05-10

Modal 弹窗

弹窗 —— 内置 portal、focus trap、滚动锁、tone 变体、异步确认、可拖拽、可缩放、命令式服务、多模态栈管理。

基础用法

v-model:open / open + onOpenChange 双向绑定。组件内部自动处理:portal 到 body、focus trap、body 滚动锁、Esc 关闭、遮罩关闭、进出 fade + scale 动画。

背景
<script setup lang="ts">
import { ref } from 'vue';
import { CfButton, CfModal } from '@chufix-design/vue';

const open = ref(false);
</script>

<template>
  <CfButton @click="open = true">打开 Modal</CfButton>
  <CfModal v-model:open="open" title="确认操作">
    <p style="margin: 0;">这是一个最简弹窗。点击遮罩 / Esc / × 都能关闭。</p>
    <template #footer>
      <CfButton variant="ghost" @click="open = false">取消</CfButton>
      <CfButton @click="open = false">确定</CfButton>
    </template>
  </CfModal>
</template>

尺寸

size 控制弹窗最大宽度。full 几乎铺满视口;width / min-height 可自定义具体像素。

背景
<script setup lang="ts">
import { ref } from 'vue';
import { CfButton, CfModal } from '@chufix-design/vue';

const sm = ref(false);
const md = ref(false);
const lg = ref(false);
const xl = ref(false);
const full = ref(false);
</script>

<template>
  <div class="demo-row">
    <CfButton variant="tertiary" @click="sm = true">sm · 360px</CfButton>
    <CfButton variant="tertiary" @click="md = true">md · 480px</CfButton>
    <CfButton variant="tertiary" @click="lg = true">lg · 640px</CfButton>
    <CfButton variant="tertiary" @click="xl = true">xl · 800px</CfButton>
    <CfButton variant="tertiary" @click="full = true">full · 几乎铺满</CfButton>
  </div>

  <CfModal v-model:open="sm" size="sm" title="size = sm">
    <p style="margin: 0;">最大宽度 360px,适合精简的确认弹窗。</p>
  </CfModal>
  <CfModal v-model:open="md" size="md" title="size = md(默认)">
    <p style="margin: 0;">最大宽度 480px,是大多数表单的合理尺寸。</p>
  </CfModal>
  <CfModal v-model:open="lg" size="lg" title="size = lg">
    <p style="margin: 0;">最大宽度 640px,适合内容较多的弹窗。</p>
  </CfModal>
  <CfModal v-model:open="xl" size="xl" title="size = xl">
    <p style="margin: 0;">最大宽度 800px,适合复杂表单或预览面板。</p>
  </CfModal>
  <CfModal v-model:open="full" size="full" title="size = full">
    <p style="margin: 0;">几乎铺满视口(保留 1.5rem 外边距),适合设置面板或全屏预览。</p>
  </CfModal>
</template>

关闭行为

默认任何操作都能关闭:遮罩点击、Esc、右上角 ×。可以通过 closeOnOverlay / closeOnEsc / showClose 单独禁用。异步态(onBeforeOk 进行中)会自动屏蔽所有关闭路径。

背景
<script setup lang="ts">
import { ref } from 'vue';
import { CfButton, CfModal } from '@chufix-design/vue';

const a = ref(false);
const b = ref(false);
</script>

<template>
  <div class="demo-row">
    <CfButton variant="tertiary" @click="a = true">禁用遮罩关闭</CfButton>
    <CfButton variant="tertiary" @click="b = true">隐藏 × 按钮</CfButton>
  </div>

  <CfModal v-model:open="a" title="只能从底部关闭" :close-on-overlay="false">
    <p style="margin: 0;">点遮罩不会关闭,只能用 Esc 或下面的按钮。</p>
    <template #footer>
      <CfButton @click="a = false">我知道了</CfButton>
    </template>
  </CfModal>

  <CfModal v-model:open="b" title="无右上角 ×" :show-close="false">
    <p style="margin: 0;">这个 Modal 隐藏了右上角的关闭按钮,必须靠 footer 操作或 Esc 关闭。</p>
    <template #footer>
      <CfButton @click="b = false">关闭</CfButton>
    </template>
  </CfModal>
</template>

Tone 变体

tone="info | success | warning | error" 给 header 加一个圆形 tone 图标 + 强调色。tone="error" 时默认 OK 按钮自动切到 danger 红。

背景
<script setup lang="ts">
import { ref } from 'vue';
import { CfButton, CfModal } from '@chufix-design/vue';

const open = ref<'info' | 'success' | 'warning' | 'error' | null>(null);
function set(t: typeof open.value) { open.value = t; }
</script>

<template>
  <div style="display: flex; gap: 8px; flex-wrap: wrap;">
    <CfButton variant="tertiary" @click="set('info')">Info</CfButton>
    <CfButton variant="tertiary" @click="set('success')">Success</CfButton>
    <CfButton variant="tertiary" @click="set('warning')">Warning</CfButton>
    <CfButton variant="danger" @click="set('error')">Error</CfButton>
  </div>

  <CfModal
    :open="open === 'info'"
    @update:open="(v) => set(v ? 'info' : null)"
    tone="info"
    title="新功能上线"
    description="表格组件现已支持双向虚拟化与 ResizeObserver 自动测高。"
    ok-text="知道了"
  />
  <CfModal
    :open="open === 'success'"
    @update:open="(v) => set(v ? 'success' : null)"
    tone="success"
    title="部署成功"
    description="v0.1.5 已发布到生产环境。"
    ok-text="完成"
  />
  <CfModal
    :open="open === 'warning'"
    @update:open="(v) => set(v ? 'warning' : null)"
    tone="warning"
    title="离开当前页面?"
    description="未保存的更改会丢失。"
    ok-text="离开"
    cancel-text="留下"
  />
  <CfModal
    :open="open === 'error'"
    @update:open="(v) => set(v ? 'error' : null)"
    tone="error"
    title="删除工作区"
    description="工作区内的所有数据将被立即清除,且无法恢复。"
    ok-text="确认删除"
    cancel-text="取消"
  />
</template>

内置 OK / Cancel + 异步确认

直接传 okText / cancelText,组件渲染默认按钮组。onBeforeOk 接异步函数:返回 false / 抛错 → 阻止关闭并恢复 loading 态;正常 resolve → 关闭并发 @ok 事件。

背景
<script setup lang="ts">
import { ref } from 'vue';
import { CfButton, CfModal, CfInput } from '@chufix-design/vue';

const open = ref(false);
const phrase = ref('');

async function onBeforeOk() {
  // 模拟服务端校验:必须输入 'delete'
  await new Promise((r) => setTimeout(r, 800));
  if (phrase.value !== 'delete') {
    // 阻止关闭
    return false;
  }
  // 真删
  await new Promise((r) => setTimeout(r, 600));
}
</script>

<template>
  <CfButton variant="danger" @click="open = true">删除项目(异步)</CfButton>

  <CfModal
    v-model:open="open"
    tone="error"
    title="确认删除"
    description="这是不可撤销的操作。请输入 'delete' 确认。"
    :on-before-ok="onBeforeOk"
    ok-text="确认删除"
    cancel-text="取消"
  >
    <CfInput v-model="phrase" placeholder="输入 delete 启用确认按钮" />
  </CfModal>
</template>

拖拽 + 缩放

draggable 让标题栏变成拖手柄;resizable 在右下角加一个对角线缩放把柄。两个能力都用 PointerEvent 实现,0 第三方依赖。

背景
<script setup lang="ts">
import { ref } from 'vue';
import { CfButton, CfModal } from '@chufix-design/vue';

const open = ref(false);
</script>

<template>
  <CfButton @click="open = true">打开可拖拽 + 可缩放</CfButton>
  <CfModal
    v-model:open="open"
    title="可移动 / 可缩放"
    description="拖标题栏移动;右下角拖把柄缩放最小 280×160。"
    draggable
    resizable
    :centered="false"
    ok-text="完成"
    cancel-text="取消"
  >
    <p style="line-height: 1.6;">
      在桌面应用风格的工具型 modal 里很常用:用户希望并排看背景内容时移动 modal,或拉大到屏幕大小看长内容。
    </p>
  </CfModal>
</template>

命令式服务

modal.confirm / danger / alert / info / success / warning / error 直接弹一个对话框并返回 Promise<boolean> —— 没有任何状态绑定、没有模板嵌套。最适合那种”先确认再操作”的辅助流程。

import { modal } from '@chufix-design/vue'; // React 端:从 @chufix-design/react 导入

const ok = await modal.confirm({
  title: '提交订单?',
  description: '提交后无法撤销',
  onOk: async () => {
    const r = await api.submit(); // 抛错或返回 false 都阻止关闭
    if (!r.ok) return false;
  },
});
if (ok) modal.success({ title: '已提交' });
背景
<script setup lang="ts">
import { CfButton, modal } from '@chufix-design/vue';

async function onConfirm() {
  const ok = await modal.confirm({
    title: '提交订单?',
    description: '提交后无法撤销。',
    onOk: async () => {
      await new Promise((r) => setTimeout(r, 800));
      // 返回 false 阻止关闭
    },
  });
  if (ok) modal.success({ title: '订单已提交', description: '我们已经发邮件给你确认。' });
}

async function onDanger() {
  const ok = await modal.danger({
    title: '清空回收站?',
    description: '所有 18 个文件将永久删除。',
  });
  if (ok) modal.info({ title: '已清空' });
}

function onAlert() {
  modal.warning({
    title: '余额不足',
    description: '当前 ¥12.00,本次消费 ¥38.00。请先充值。',
  });
}
</script>

<template>
  <div style="display: flex; gap: 8px; flex-wrap: wrap;">
    <CfButton variant="primary" @click="onConfirm">异步 confirm</CfButton>
    <CfButton variant="danger" @click="onDanger">danger</CfButton>
    <CfButton variant="tertiary" @click="onAlert">warning alert</CfButton>
  </div>
</template>

服务方法签名:

方法tone默认按钮
modal.open(opts)跟 opts 一致跟 opts
modal.confirm(opts)warningOK + Cancel
modal.danger(opts)error红色 OK + Cancel
modal.alert(opts)infoOK
modal.info / success / warning / error(opts)同名 toneOK

多模态栈

每打开一个 Modal,组件内部把它的 close 函数压进栈,z-index 自动 +10;Esc 只关最顶层;关闭后从栈里弹出。所以你可以在 modal 里再触发 modal,不用关心层级。

背景
<script setup lang="ts">
import { ref } from 'vue';
import { CfButton, CfModal } from '@chufix-design/vue';

const layer1 = ref(false);
const layer2 = ref(false);
</script>

<template>
  <CfButton @click="layer1 = true">打开第一层</CfButton>

  <CfModal
    v-model:open="layer1"
    title="第一层"
    description="可以在这一层再打开嵌套的对话框。"
    ok-text="确定"
    cancel-text="取消"
  >
    <p style="line-height: 1.6; margin-bottom: 12px;">两层 modal 同时存在时,组件维护内部 z-index 栈,每层自动 +10。Esc 只关闭最顶层。</p>
    <CfButton variant="tertiary" @click="layer2 = true">打开第二层</CfButton>

    <CfModal
      v-model:open="layer2"
      tone="warning"
      title="第二层"
      description="这一层是从第一层里弹出来的;遮罩仍然位于第一层之上。"
      ok-text="知道了"
    />
  </CfModal>
</template>

API

Prop类型默认说明
openbooleanfalse受控开关
titlestringheader 标题文字
descriptionstring标题下的描述(一行更弱的副标题)
size'sm' | 'md' | 'lg' | 'xl' | 'full''md'最大宽度档位
tone'default' | 'info' | 'success' | 'warning' | 'error''default'视觉 tone + 自动图标
centeredbooleantrue垂直居中或顶部 80px 起
widthnumber | string自定义宽度(覆盖 size)
minHeightnumber | string自定义最小高度
closeOnOverlaybooleantrue点遮罩是否关闭
closeOnEscbooleantrueEsc 是否关闭
showClosebooleantrue右上角 ×
footerAlign'start' | 'center' | 'end' | 'space-between''end'footer 对齐
draggablebooleanfalse拖标题移动
resizablebooleanfalse右下角拖拽缩放
okText / cancelTextstring默认按钮文案;不传则不渲染默认按钮
okVariant'primary' | 'danger' | 'secondary''primary'确认按钮样式
onBeforeOk() => boolean | void | Promise<...>异步钩子;false / throw 阻止关闭
tostring | Element'body'Teleport / Portal 目标容器
zIndexnumber自动栈管理自定义 z-index 起点

插槽 / 事件

  • Vue:header / 默认 / footer 三个具名插槽。footer 插槽接收 { ok, cancel, loading } 用于自渲染按钮。
  • React:headerfooter(可传 ReactNode 或 ({ ok, cancel, loading }) => ReactNode)+ children
  • 事件:update:open / close / ok / cancel

反馈与讨论

Modal 弹窗 的讨论

0
0 / 600
一键发送
正在加载评论...