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

Modal 弹窗

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

基础用法

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

背景 视口
src/App.vue
<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>
<script setup>
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>
import { useState } from 'react';
import { CfButton, CfModal } from '@chufix-design/react';

export default function Demo() {
  const [open, setOpen] = useState(false);
  return (
    <>
      <CfButton onClick={() => setOpen(true)}>打开 Modal</CfButton>
        <CfModal open={open} onOpenChange={setOpen} title="确认操作">
          <p style={{ margin: 0 }}>这是一个最简弹窗。点击遮罩 / Esc / × 都能关闭。</p>
            <CfButton variant="ghost" onClick={() => setOpen(false)}>取消</CfButton>
            <CfButton onClick={() => setOpen(false)}>确定</CfButton>
    </>
  );
}
import { useState } from 'react';
import { CfButton, CfModal } from '@chufix-design/react';

export default function Demo() {
  const [open, setOpen] = useState(false);
  return (
    <>
      <CfButton onClick={() => setOpen(true)}>打开 Modal</CfButton>
        <CfModal open={open} onOpenChange={setOpen} title="确认操作">
          <p style={{ margin: 0 }}>这是一个最简弹窗。点击遮罩 / Esc / × 都能关闭。</p>
            <CfButton variant="ghost" onClick={() => setOpen(false)}>取消</CfButton>
            <CfButton onClick={() => setOpen(false)}>确定</CfButton>
    </>
  );
}

尺寸

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

背景 视口
src/App.vue
<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>
<script setup>
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>
import { useState } from 'react';
import { CfButton, CfModal } from '@chufix-design/react';

export default function Demo() {
  const [sm, setSm] = useState(false);
  const [md, setMd] = useState(false);
  const [lg, setLg] = useState(false);
  const [xl, setXl] = useState(false);
  const [full, setFull] = useState(false);
  return (
    <>
      <div className="demo-row">
          <CfButton variant="tertiary" onClick={() => setSm(true)}>sm · 360px</CfButton>
          <CfButton variant="tertiary" onClick={() => setMd(true)}>md · 480px</CfButton>
          <CfButton variant="tertiary" onClick={() => setLg(true)}>lg · 640px</CfButton>
          <CfButton variant="tertiary" onClick={() => setXl(true)}>xl · 800px</CfButton>
          <CfButton variant="tertiary" onClick={() => setFull(true)}>full · 几乎铺满</CfButton>
        </div>
        <CfModal open={sm} onOpenChange={setSm} size="sm" title="size = sm">
          <p style={{ margin: 0 }}>最大宽度 360px,适合精简的确认弹窗。</p>
        </CfModal>
        <CfModal open={md} onOpenChange={setMd} size="md" title="size = md(默认)">
          <p style={{ margin: 0 }}>最大宽度 480px,是大多数表单的合理尺寸。</p>
        </CfModal>
        <CfModal open={lg} onOpenChange={setLg} size="lg" title="size = lg">
          <p style={{ margin: 0 }}>最大宽度 640px,适合内容较多的弹窗。</p>
        </CfModal>
        <CfModal open={xl} onOpenChange={setXl} size="xl" title="size = xl">
          <p style={{ margin: 0 }}>最大宽度 800px,适合复杂表单或预览面板。</p>
        </CfModal>
        <CfModal open={full} onOpenChange={setFull} size="full" title="size = full">
          <p style={{ margin: 0 }}>几乎铺满视口(保留 1.5rem 外边距),适合设置面板或全屏预览。</p>
        </CfModal>
    </>
  );
}
import { useState } from 'react';
import { CfButton, CfModal } from '@chufix-design/react';

export default function Demo() {
  const [sm, setSm] = useState(false);
  const [md, setMd] = useState(false);
  const [lg, setLg] = useState(false);
  const [xl, setXl] = useState(false);
  const [full, setFull] = useState(false);
  return (
    <>
      <div className="demo-row">
          <CfButton variant="tertiary" onClick={() => setSm(true)}>sm · 360px</CfButton>
          <CfButton variant="tertiary" onClick={() => setMd(true)}>md · 480px</CfButton>
          <CfButton variant="tertiary" onClick={() => setLg(true)}>lg · 640px</CfButton>
          <CfButton variant="tertiary" onClick={() => setXl(true)}>xl · 800px</CfButton>
          <CfButton variant="tertiary" onClick={() => setFull(true)}>full · 几乎铺满</CfButton>
        </div>
        <CfModal open={sm} onOpenChange={setSm} size="sm" title="size = sm">
          <p style={{ margin: 0 }}>最大宽度 360px,适合精简的确认弹窗。</p>
        </CfModal>
        <CfModal open={md} onOpenChange={setMd} size="md" title="size = md(默认)">
          <p style={{ margin: 0 }}>最大宽度 480px,是大多数表单的合理尺寸。</p>
        </CfModal>
        <CfModal open={lg} onOpenChange={setLg} size="lg" title="size = lg">
          <p style={{ margin: 0 }}>最大宽度 640px,适合内容较多的弹窗。</p>
        </CfModal>
        <CfModal open={xl} onOpenChange={setXl} size="xl" title="size = xl">
          <p style={{ margin: 0 }}>最大宽度 800px,适合复杂表单或预览面板。</p>
        </CfModal>
        <CfModal open={full} onOpenChange={setFull} size="full" title="size = full">
          <p style={{ margin: 0 }}>几乎铺满视口(保留 1.5rem 外边距),适合设置面板或全屏预览。</p>
        </CfModal>
    </>
  );
}

关闭行为

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

背景 视口
src/App.vue
<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>
<script setup>
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>
import { useState } from 'react';
import { CfButton, CfModal } from '@chufix-design/react';

export default function Demo() {
  const [a, setA] = useState(false);
  const [b, setB] = useState(false);
  return (
    <>
      <div className="demo-row">
          <CfButton variant="tertiary" onClick={() => setA(true)}>禁用遮罩关闭</CfButton>
          <CfButton variant="tertiary" onClick={() => setB(true)}>隐藏 × 按钮</CfButton>
        </div>
        <CfModal open={a} onOpenChange={setA} title="只能从底部关闭" closeOnOverlay={false}>
          <p style={{ margin: 0 }}>点遮罩不会关闭,只能用 Esc 或下面的按钮。</p>
            <CfButton onClick={() => setA(false)}>我知道了</CfButton>
    </>
  );
}
import { useState } from 'react';
import { CfButton, CfModal } from '@chufix-design/react';

export default function Demo() {
  const [a, setA] = useState(false);
  const [b, setB] = useState(false);
  return (
    <>
      <div className="demo-row">
          <CfButton variant="tertiary" onClick={() => setA(true)}>禁用遮罩关闭</CfButton>
          <CfButton variant="tertiary" onClick={() => setB(true)}>隐藏 × 按钮</CfButton>
        </div>
        <CfModal open={a} onOpenChange={setA} title="只能从底部关闭" closeOnOverlay={false}>
          <p style={{ margin: 0 }}>点遮罩不会关闭,只能用 Esc 或下面的按钮。</p>
            <CfButton onClick={() => setA(false)}>我知道了</CfButton>
    </>
  );
}

Tone 变体

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

背景 视口
src/App.vue
<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>
<script setup>
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>
import { useState } from 'react';
import { CfButton, CfModal } from '@chufix-design/react';

export default function Demo() {
  const [open, setOpen] = useState<'info' | 'success' | 'warning' | 'error' | null>(null);
  function set(t: typeof open) { setOpen(t); }
  return (
    <>
      <div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
          <CfButton variant="tertiary" onClick={() => set('info')}>Info</CfButton>
          <CfButton variant="tertiary" onClick={() => set('success')}>Success</CfButton>
          <CfButton variant="tertiary" onClick={() => set('warning')}>Warning</CfButton>
          <CfButton variant="danger" onClick={() => set('error')}>Error</CfButton>
        </div>
        <CfModal open={open === 'info'} onOpenChange={(v) => set(v ? 'info' : null)}
          tone="info"
          title="新功能上线"
          description="表格组件现已支持双向虚拟化与 ResizeObserver 自动测高。"
          ok-text="知道了"
        />
        <CfModal open={open === 'success'} onOpenChange={(v) => set(v ? 'success' : null)}
          tone="success"
          title="部署成功"
          description="v0.1.5 已发布到生产环境。"
          ok-text="完成"
        />
        <CfModal open={open === 'warning'} onOpenChange={(v) => set(v ? 'warning' : null)}
          tone="warning"
          title="离开当前页面?"
          description="未保存的更改会丢失。"
          ok-text="离开"
          cancel-text="留下"
        />
        <CfModal open={open === 'error'} onOpenChange={(v) => set(v ? 'error' : null)}
          tone="error"
          title="删除工作区"
          description="工作区内的所有数据将被立即清除,且无法恢复。"
          ok-text="确认删除"
          cancel-text="取消"
        />
    </>
  );
}
import { useState } from 'react';
import { CfButton, CfModal } from '@chufix-design/react';

export default function Demo() {
  const [open, setOpen] = useState<'info' | 'success' | 'warning' | 'error' | null>(null);
  function set(t) { setOpen(t); }
  return (
    <>
      <div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
          <CfButton variant="tertiary" onClick={() => set('info')}>Info</CfButton>
          <CfButton variant="tertiary" onClick={() => set('success')}>Success</CfButton>
          <CfButton variant="tertiary" onClick={() => set('warning')}>Warning</CfButton>
          <CfButton variant="danger" onClick={() => set('error')}>Error</CfButton>
        </div>
        <CfModal open={open === 'info'} onOpenChange={(v) => set(v ? 'info' : null)}
          tone="info"
          title="新功能上线"
          description="表格组件现已支持双向虚拟化与 ResizeObserver 自动测高。"
          ok-text="知道了"
        />
        <CfModal open={open === 'success'} onOpenChange={(v) => set(v ? 'success' : null)}
          tone="success"
          title="部署成功"
          description="v0.1.5 已发布到生产环境。"
          ok-text="完成"
        />
        <CfModal open={open === 'warning'} onOpenChange={(v) => set(v ? 'warning' : null)}
          tone="warning"
          title="离开当前页面?"
          description="未保存的更改会丢失。"
          ok-text="离开"
          cancel-text="留下"
        />
        <CfModal open={open === 'error'} onOpenChange={(v) => set(v ? 'error' : null)}
          tone="error"
          title="删除工作区"
          description="工作区内的所有数据将被立即清除,且无法恢复。"
          ok-text="确认删除"
          cancel-text="取消"
        />
    </>
  );
}

内置 OK / Cancel + 异步确认

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

背景 视口
src/App.vue
<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>
<script setup>
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>
import { useState } from 'react';
import { CfButton, CfInput, CfModal } from '@chufix-design/react';

export default function Demo() {
  const [open, setOpen] = useState(false);
  const [phrase, setPhrase] = useState('');

  async function onBeforeOk() {
    // 模拟服务端校验:必须输入 'delete'
    await new Promise((r) => setTimeout(r, 800));
    if (phrase !== 'delete') {
      // 阻止关闭
      return false;
    }
    // 真删
    await new Promise((r) => setTimeout(r, 600));
  }
  return (
    <>
      <CfButton variant="danger" onClick={() => setOpen(true)}>删除项目(异步)</CfButton>
        <CfModal open={open} onOpenChange={setOpen} tone="error" title="确认删除" description="这是不可撤销的操作。请输入 'delete' 确认。" onBeforeOk={onBeforeOk} ok-text="确认删除" cancel-text="取消" >
          <CfInput value={phrase} onChange={setPhrase} placeholder="输入 delete 启用确认按钮" />
        </CfModal>
    </>
  );
}
import { useState } from 'react';
import { CfButton, CfInput, CfModal } from '@chufix-design/react';

export default function Demo() {
  const [open, setOpen] = useState(false);
  const [phrase, setPhrase] = useState('');

  async function onBeforeOk() {
    // 模拟服务端校验:必须输入 'delete'
    await new Promise((r) => setTimeout(r, 800));
    if (phrase !== 'delete') {
      // 阻止关闭
      return false;
    }
    // 真删
    await new Promise((r) => setTimeout(r, 600));
  }
  return (
    <>
      <CfButton variant="danger" onClick={() => setOpen(true)}>删除项目(异步)</CfButton>
        <CfModal open={open} onOpenChange={setOpen} tone="error" title="确认删除" description="这是不可撤销的操作。请输入 'delete' 确认。" onBeforeOk={onBeforeOk} ok-text="确认删除" cancel-text="取消" >
          <CfInput value={phrase} onChange={setPhrase} placeholder="输入 delete 启用确认按钮" />
        </CfModal>
    </>
  );
}

拖拽 + 缩放

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

背景 视口
src/App.vue
<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>
<script setup>
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>
import { useState } from 'react';
import { CfButton, CfModal } from '@chufix-design/react';

export default function Demo() {
  const [open, setOpen] = useState(false);
  return (
    <>
      <CfButton onClick={() => setOpen(true)}>打开可拖拽 + 可缩放</CfButton>
        <CfModal open={open} onOpenChange={setOpen} title="可移动 / 可缩放" description="拖标题栏移动;右下角拖把柄缩放最小 280×160。" draggable resizable centered={false} ok-text="完成" cancel-text="取消" >
          <p style={{ lineHeight: 1.6 }}>
            在桌面应用风格的工具型 modal 里很常用:用户希望并排看背景内容时移动 modal,或拉大到屏幕大小看长内容。
          </p>
        </CfModal>
    </>
  );
}
import { useState } from 'react';
import { CfButton, CfModal } from '@chufix-design/react';

export default function Demo() {
  const [open, setOpen] = useState(false);
  return (
    <>
      <CfButton onClick={() => setOpen(true)}>打开可拖拽 + 可缩放</CfButton>
        <CfModal open={open} onOpenChange={setOpen} title="可移动 / 可缩放" description="拖标题栏移动;右下角拖把柄缩放最小 280×160。" draggable resizable centered={false} ok-text="完成" cancel-text="取消" >
          <p style={{ lineHeight: 1.6 }}>
            在桌面应用风格的工具型 modal 里很常用:用户希望并排看背景内容时移动 modal,或拉大到屏幕大小看长内容。
          </p>
        </CfModal>
    </>
  );
}

命令式服务

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: '已提交' });
背景 视口
src/App.vue
<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>
<script setup>
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>
import { CfButton } from '@chufix-design/react';

export default function Demo() {
  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。请先充值。',
    });
  }
  return (
    <>
      <div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
          <CfButton variant="primary" onClick={onConfirm}>异步 confirm</CfButton>
          <CfButton variant="danger" onClick={onDanger}>danger</CfButton>
          <CfButton variant="tertiary" onClick={onAlert}>warning alert</CfButton>
        </div>
    </>
  );
}
import { CfButton } from '@chufix-design/react';

export default function Demo() {
  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。请先充值。',
    });
  }
  return (
    <>
      <div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
          <CfButton variant="primary" onClick={onConfirm}>异步 confirm</CfButton>
          <CfButton variant="danger" onClick={onDanger}>danger</CfButton>
          <CfButton variant="tertiary" onClick={onAlert}>warning alert</CfButton>
        </div>
    </>
  );
}

服务方法签名:

方法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

footer 具名插槽接收 { ok, cancel, loading } 三个作用域参数,可以完全自渲染按钮组: 插入”保存草稿”、“导出 PDF”、“导出 Excel” 等额外动作,同时复用组件内置的 ok/cancel 关闭逻辑与 onBeforeOk 等待态。

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

const open = ref<boolean>(false);
const saving = ref<boolean>(false);

async function saveDraft(): Promise<void> {
  saving.value = true;
  await new Promise<void>((resolve) => setTimeout(resolve, 600));
  saving.value = false;
  open.value = false;
}
</script>
<template>
  <CfButton variant="primary" @click="open = true">打开自定义 footer</CfButton>
  <CfModal v-model:open="open" title="导出报告" size="sm">
    <p>选择要导出的格式,并决定是否同时保存草稿。</p>
    <template #footer="{ ok, cancel, loading }">
      <CfButton variant="ghost" :disabled="loading" @click="cancel">
        取消
      </CfButton>
      <CfButton variant="tertiary" :loading="saving" @click="saveDraft">
        保存草稿
      </CfButton>
      <CfButton variant="primary" :loading="loading" @click="ok">
        导出 PDF
      </CfButton>
    </template>
  </CfModal>
</template>
<script setup>
import { ref } from 'vue';
import { CfButton, CfModal } from '@chufix-design/vue';

const open = ref<boolean>(false);
const saving = ref<boolean>(false);

async function saveDraft(): Promise<void> {
  saving.value = true;
  await new Promise<void>((resolve) => setTimeout(resolve, 600));
  saving.value = false;
  open.value = false;
}
</script>
<template>
  <CfButton variant="primary" @click="open = true">打开自定义 footer</CfButton>
  <CfModal v-model:open="open" title="导出报告" size="sm">
    <p>选择要导出的格式,并决定是否同时保存草稿。</p>
    <template #footer="{ ok, cancel, loading }">
      <CfButton variant="ghost" :disabled="loading" @click="cancel">
        取消
      </CfButton>
      <CfButton variant="tertiary" :loading="saving" @click="saveDraft">
        保存草稿
      </CfButton>
      <CfButton variant="primary" :loading="loading" @click="ok">
        导出 PDF
      </CfButton>
    </template>
  </CfModal>
</template>
import { useState } from 'react';
import { CfButton, CfModal } from '@chufix-design/react';

export default function Demo() {
  const [open, setOpen] = useState<boolean>(false);
  const [saving, setSaving] = useState<boolean>(false);

  async function saveDraft(): Promise<void> {
    setSaving(true);
    await new Promise<void>((resolve) => setTimeout(resolve, 600));
    setSaving(false);
    setOpen(false);
  }
  return (
    <>
      <CfButton variant="primary" onClick={() => setOpen(true)}>打开自定义 footer</CfButton>
        <CfModal open={open} onOpenChange={setOpen} title="导出报告" size="sm">
          <p>选择要导出的格式,并决定是否同时保存草稿。</p>
            <CfButton variant="ghost" disabled={loading} onClick={cancel}>
              取消
            </CfButton>
            <CfButton variant="tertiary" loading={saving} onClick={saveDraft}>
              保存草稿
            </CfButton>
            <CfButton variant="primary" loading={loading} onClick={ok}>
              导出 PDF
            </CfButton>
    </>
  );
}
import { useState } from 'react';
import { CfButton, CfModal } from '@chufix-design/react';

export default function Demo() {
  const [open, setOpen] = useState<boolean>(false);
  const [saving, setSaving] = useState<boolean>(false);

  async function saveDraft(): Promise<void> {
    setSaving(true);
    await new Promise<void>((resolve) => setTimeout(resolve, 600));
    setSaving(false);
    setOpen(false);
  }
  return (
    <>
      <CfButton variant="primary" onClick={() => setOpen(true)}>打开自定义 footer</CfButton>
        <CfModal open={open} onOpenChange={setOpen} title="导出报告" size="sm">
          <p>选择要导出的格式,并决定是否同时保存草稿。</p>
            <CfButton variant="ghost" disabled={loading} onClick={cancel}>
              取消
            </CfButton>
            <CfButton variant="tertiary" loading={saving} onClick={saveDraft}>
              保存草稿
            </CfButton>
            <CfButton variant="primary" loading={loading} onClick={ok}>
              导出 PDF
            </CfButton>
    </>
  );
}

确认删除

不可逆操作的标准弹窗:tone="error" 给出红色配色与图标,okVariant="danger" 让删除按钮也变红, onBeforeOk 异步等待后端响应;返回 false 时阻止关闭,留时间显示错误反馈。

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

const open = ref<boolean>(false);
const target = ref<string>('config-2026-q2.json');

async function doDelete(): Promise<boolean> {
  await new Promise<void>((resolve) => setTimeout(resolve, 700));
  // 演示态:随机模拟一次失败用于展示 onBeforeOk 阻止关闭
  if (Math.random() < 0) {
    toast.error('删除失败');
    return false;
  }
  toast.success(`已删除 ${target.value}`);
  return true;
}
</script>
<template>
  <CfButton variant="danger" @click="open = true">删除文件…</CfButton>
  <CfModal
    v-model:open="open"
    tone="error"
    title="确认删除?"
    :description="`将永久删除 ${target},无法撤销。`"
    size="sm"
    ok-text="删除"
    ok-variant="danger"
    cancel-text="取消"
    :on-before-ok="doDelete"
  />
</template>
<script setup>
import { ref } from 'vue';
import { CfButton, CfModal, toast } from '@chufix-design/vue';

const open = ref<boolean>(false);
const target = ref<string>('config-2026-q2.json');

async function doDelete(): Promise<boolean> {
  await new Promise<void>((resolve) => setTimeout(resolve, 700));
  // 演示态:随机模拟一次失败用于展示 onBeforeOk 阻止关闭
  if (Math.random() < 0) {
    toast.error('删除失败');
    return false;
  }
  toast.success(`已删除 ${target.value}`);
  return true;
}
</script>
<template>
  <CfButton variant="danger" @click="open = true">删除文件…</CfButton>
  <CfModal
    v-model:open="open"
    tone="error"
    title="确认删除?"
    :description="`将永久删除 ${target},无法撤销。`"
    size="sm"
    ok-text="删除"
    ok-variant="danger"
    cancel-text="取消"
    :on-before-ok="doDelete"
  />
</template>
import { useState } from 'react';
import { CfButton, CfModal } from '@chufix-design/react';

export default function Demo() {
  const [open, setOpen] = useState<boolean>(false);
  const [target, setTarget] = useState<string>('config-2026-q2.json');

  async function doDelete(): Promise<boolean> {
    await new Promise<void>((resolve) => setTimeout(resolve, 700));
    // 演示态:随机模拟一次失败用于展示 onBeforeOk 阻止关闭
    if (Math.random() < 0) {
      toast.error('删除失败');
      return false;
    }
    toast.success(`已删除 ${target}`);
    return true;
  }
  return (
    <>
      <CfButton variant="danger" onClick={() => setOpen(true)}>删除文件…</CfButton>
        <CfModal open={open} onOpenChange={setOpen} tone="error" title="确认删除?" description={`将永久删除 ${target},无法撤销。`} size="sm" ok-text="删除" ok-variant="danger" cancel-text="取消" onBeforeOk={doDelete} />
    </>
  );
}
import { useState } from 'react';
import { CfButton, CfModal } from '@chufix-design/react';

export default function Demo() {
  const [open, setOpen] = useState<boolean>(false);
  const [target, setTarget] = useState<string>('config-2026-q2.json');

  async function doDelete(): Promise<boolean> {
    await new Promise<void>((resolve) => setTimeout(resolve, 700));
    // 演示态:随机模拟一次失败用于展示 onBeforeOk 阻止关闭
    if (Math.random() < 0) {
      toast.error('删除失败');
      return false;
    }
    toast.success(`已删除 ${target}`);
    return true;
  }
  return (
    <>
      <CfButton variant="danger" onClick={() => setOpen(true)}>删除文件…</CfButton>
        <CfModal open={open} onOpenChange={setOpen} tone="error" title="确认删除?" description={`将永久删除 ${target},无法撤销。`} size="sm" ok-text="删除" ok-variant="danger" cancel-text="取消" onBeforeOk={doDelete} />
    </>
  );
}

多模态栈

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

背景 视口
src/App.vue
<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>
<script setup>
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>
import { useState } from 'react';
import { CfButton, CfModal } from '@chufix-design/react';

export default function Demo() {
  const [layer1, setLayer1] = useState(false);
  const [layer2, setLayer2] = useState(false);
  return (
    <>
      <CfButton onClick={() => setLayer1(true)}>打开第一层</CfButton>
        <CfModal open={layer1} onOpenChange={setLayer1} title="第一层" description="可以在这一层再打开嵌套的对话框。" ok-text="确定" cancel-text="取消" >
          <p style={{ lineHeight: 1.6, marginBottom: 12 }}>两层 modal 同时存在时,组件维护内部 z-index 栈,每层自动 +10。Esc 只关闭最顶层。</p>
          <CfButton variant="tertiary" onClick={() => setLayer2(true)}>打开第二层</CfButton>
          <CfModal open={layer2} onOpenChange={setLayer2} tone="warning" title="第二层" description="这一层是从第一层里弹出来的;遮罩仍然位于第一层之上。" ok-text="知道了" />
        </CfModal>
    </>
  );
}
import { useState } from 'react';
import { CfButton, CfModal } from '@chufix-design/react';

export default function Demo() {
  const [layer1, setLayer1] = useState(false);
  const [layer2, setLayer2] = useState(false);
  return (
    <>
      <CfButton onClick={() => setLayer1(true)}>打开第一层</CfButton>
        <CfModal open={layer1} onOpenChange={setLayer1} title="第一层" description="可以在这一层再打开嵌套的对话框。" ok-text="确定" cancel-text="取消" >
          <p style={{ lineHeight: 1.6, marginBottom: 12 }}>两层 modal 同时存在时,组件维护内部 z-index 栈,每层自动 +10。Esc 只关闭最顶层。</p>
          <CfButton variant="tertiary" onClick={() => setLayer2(true)}>打开第二层</CfButton>
          <CfModal open={layer2} onOpenChange={setLayer2} tone="warning" title="第二层" description="这一层是从第一层里弹出来的;遮罩仍然位于第一层之上。" ok-text="知道了" />
        </CfModal>
    </>
  );
}

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
正在加载评论...