Drawer 抽屉
边缘锚定的滑入面板 —— 4 个方向、tone 变体、可缩放、异步确认、不锁背景模式、命令式服务、多抽屉栈管理。
基础用法
v-model:open (Vue) / open + onOpenChange (React) 双向绑定。Drawer 共用 Modal 的 portal / focus trap / body 滚动锁 / Esc 关闭基础设施。
<script setup lang="ts">
import { ref } from 'vue';
import { CfButton, CfDrawer } from '@chufix-design/vue';
const open = ref(false);
</script>
<template>
<CfButton @click="open = true">打开抽屉</CfButton>
<CfDrawer 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>
</CfDrawer>
</template> <script setup>
import { ref } from 'vue';
import { CfButton, CfDrawer } from '@chufix-design/vue';
const open = ref(false);
</script>
<template>
<CfButton @click="open = true">打开抽屉</CfButton>
<CfDrawer 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>
</CfDrawer>
</template> 4 种滑入方向
placement 决定从哪一边滑入 — 右(默认,最常见的详情侧栏)/ 左(移动端导航)/ 顶 / 底(playground 风格的临时操作)。
<script setup lang="ts">
import { ref } from 'vue';
import { CfButton, CfDrawer } from '@chufix-design/vue';
const right = ref(false);
const left = ref(false);
const top = ref(false);
const bottom = ref(false);
</script>
<template>
<div style="display: flex; flex-wrap: wrap; gap: 8px;">
<CfButton @click="right = true">从右侧</CfButton>
<CfButton variant="secondary" @click="left = true">从左侧</CfButton>
<CfButton variant="secondary" @click="top = true">从顶部</CfButton>
<CfButton variant="secondary" @click="bottom = true">从底部</CfButton>
</div>
<CfDrawer v-model:open="right" title="右侧抽屉" placement="right" />
<CfDrawer v-model:open="left" title="左侧抽屉" placement="left" />
<CfDrawer v-model:open="top" title="顶部抽屉" placement="top" />
<CfDrawer v-model:open="bottom" title="底部抽屉" placement="bottom" />
</template> <script setup>
import { ref } from 'vue';
import { CfButton, CfDrawer } from '@chufix-design/vue';
const right = ref(false);
const left = ref(false);
const top = ref(false);
const bottom = ref(false);
</script>
<template>
<div style="display: flex; flex-wrap: wrap; gap: 8px;">
<CfButton @click="right = true">从右侧</CfButton>
<CfButton variant="secondary" @click="left = true">从左侧</CfButton>
<CfButton variant="secondary" @click="top = true">从顶部</CfButton>
<CfButton variant="secondary" @click="bottom = true">从底部</CfButton>
</div>
<CfDrawer v-model:open="right" title="右侧抽屉" placement="right" />
<CfDrawer v-model:open="left" title="左侧抽屉" placement="left" />
<CfDrawer v-model:open="top" title="顶部抽屉" placement="top" />
<CfDrawer v-model:open="bottom" title="底部抽屉" placement="bottom" />
</template> 5 档尺寸
size 控制抽屉宽度(左右 placement)或高度(上下 placement) — sm / md(默认)/ lg / xl / full(全屏)。width / height 也可以传具体像素值覆盖档位。
<script setup lang="ts">
import { ref } from 'vue';
import { CfButton, CfDrawer } 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 style="display: flex; flex-wrap: wrap; gap: 8px;">
<CfButton @click="sm = true">sm</CfButton>
<CfButton @click="md = true">md</CfButton>
<CfButton @click="lg = true">lg</CfButton>
<CfButton @click="xl = true">xl</CfButton>
<CfButton @click="full = true">full</CfButton>
</div>
<CfDrawer v-model:open="sm" title="size = sm" size="sm" />
<CfDrawer v-model:open="md" title="size = md" size="md" />
<CfDrawer v-model:open="lg" title="size = lg" size="lg" />
<CfDrawer v-model:open="xl" title="size = xl" size="xl" />
<CfDrawer v-model:open="full" title="size = full" size="full" />
</template> <script setup>
import { ref } from 'vue';
import { CfButton, CfDrawer } 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 style="display: flex; flex-wrap: wrap; gap: 8px;">
<CfButton @click="sm = true">sm</CfButton>
<CfButton @click="md = true">md</CfButton>
<CfButton @click="lg = true">lg</CfButton>
<CfButton @click="xl = true">xl</CfButton>
<CfButton @click="full = true">full</CfButton>
</div>
<CfDrawer v-model:open="sm" title="size = sm" size="sm" />
<CfDrawer v-model:open="md" title="size = md" size="md" />
<CfDrawer v-model:open="lg" title="size = lg" size="lg" />
<CfDrawer v-model:open="xl" title="size = xl" size="xl" />
<CfDrawer v-model:open="full" title="size = full" size="full" />
</template> Tone 变体
tone="info | success | warning | error" 给 header 加一个圆形 tone 图标 + 强调色。tone="error" 时默认 OK 按钮自动切到 danger 红。
<script setup lang="ts">
import { ref } from 'vue';
import { CfButton, CfDrawer } from '@chufix-design/vue';
const info = ref(false);
const success = ref(false);
const warning = ref(false);
const error = ref(false);
</script>
<template>
<div style="display: flex; flex-wrap: wrap; gap: 8px;">
<CfButton variant="tertiary" @click="info = true">info</CfButton>
<CfButton variant="tertiary" @click="success = true">success</CfButton>
<CfButton variant="tertiary" @click="warning = true">warning</CfButton>
<CfButton variant="tertiary" @click="error = true">error</CfButton>
</div>
<CfDrawer
v-model:open="info"
tone="info"
title="新版本上线"
description="0.2.0 — 拖拽面板、tone 变体、命令式 service。"
ok-text="知道了"
>
<p style="margin: 0;">使用 tone 在 header 上自动加圆形语义图标。</p>
</CfDrawer>
<CfDrawer
v-model:open="success"
tone="success"
title="部署成功"
description="cd-12 已发布到生产环境"
ok-text="完成"
>
<p style="margin: 0;">tone="success" 适合状态提示型抽屉。</p>
</CfDrawer>
<CfDrawer
v-model:open="warning"
tone="warning"
title="存在未保存的修改"
description="离开前请先保存草稿"
ok-text="保存并离开"
cancel-text="取消"
>
<p style="margin: 0;">两侧按钮 + warning 头像。</p>
</CfDrawer>
<CfDrawer
v-model:open="error"
tone="error"
title="即将永久删除项目"
description="该操作不可撤销。"
ok-text="删除"
cancel-text="取消"
>
<p style="margin: 0;">tone="error" 时默认 OK 按钮自动切到 danger 红色。</p>
</CfDrawer>
</template> <script setup>
import { ref } from 'vue';
import { CfButton, CfDrawer } from '@chufix-design/vue';
const info = ref(false);
const success = ref(false);
const warning = ref(false);
const error = ref(false);
</script>
<template>
<div style="display: flex; flex-wrap: wrap; gap: 8px;">
<CfButton variant="tertiary" @click="info = true">info</CfButton>
<CfButton variant="tertiary" @click="success = true">success</CfButton>
<CfButton variant="tertiary" @click="warning = true">warning</CfButton>
<CfButton variant="tertiary" @click="error = true">error</CfButton>
</div>
<CfDrawer
v-model:open="info"
tone="info"
title="新版本上线"
description="0.2.0 — 拖拽面板、tone 变体、命令式 service。"
ok-text="知道了"
>
<p style="margin: 0;">使用 tone 在 header 上自动加圆形语义图标。</p>
</CfDrawer>
<CfDrawer
v-model:open="success"
tone="success"
title="部署成功"
description="cd-12 已发布到生产环境"
ok-text="完成"
>
<p style="margin: 0;">tone="success" 适合状态提示型抽屉。</p>
</CfDrawer>
<CfDrawer
v-model:open="warning"
tone="warning"
title="存在未保存的修改"
description="离开前请先保存草稿"
ok-text="保存并离开"
cancel-text="取消"
>
<p style="margin: 0;">两侧按钮 + warning 头像。</p>
</CfDrawer>
<CfDrawer
v-model:open="error"
tone="error"
title="即将永久删除项目"
description="该操作不可撤销。"
ok-text="删除"
cancel-text="取消"
>
<p style="margin: 0;">tone="error" 时默认 OK 按钮自动切到 danger 红色。</p>
</CfDrawer>
</template> 内置 OK / Cancel + 异步确认
直接传 okText / cancelText,组件渲染默认按钮组。onBeforeOk 接异步函数:返回 false / 抛错 → 阻止关闭并恢复 loading 态;正常 resolve → 关闭并发 @ok 事件。
<script setup lang="ts">
import { ref } from 'vue';
import { CfButton, CfDrawer, CfInput, toast } from '@chufix-design/vue';
const open = ref(false);
const name = ref('');
function onBeforeOk(): Promise<boolean | void> {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (name.value.trim().length < 2) {
toast({ type: 'error', message: '名称至少 2 个字符' });
reject(new Error('invalid'));
return;
}
toast({ type: 'success', message: `已保存:${name.value}` });
resolve();
}, 900);
});
}
</script>
<template>
<CfButton @click="open = true">编辑配置</CfButton>
<CfDrawer
v-model:open="open"
placement="right"
size="md"
title="编辑配置"
description="保存按钮接异步 onBeforeOk —— 期间所有关闭路径屏蔽。"
ok-text="保存"
cancel-text="取消"
:on-before-ok="onBeforeOk"
@ok="open = false; name = ''"
>
<div style="display: grid; gap: 8px;">
<label style="font-size: 12px; color: var(--fg-3);">名称</label>
<CfInput v-model="name" placeholder="输入≥2字符" />
</div>
</CfDrawer>
</template> <script setup>
import { ref } from 'vue';
import { CfButton, CfDrawer, CfInput, toast } from '@chufix-design/vue';
const open = ref(false);
const name = ref('');
function onBeforeOk(): Promise<boolean | void> {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (name.value.trim().length < 2) {
toast({ type: 'error', message: '名称至少 2 个字符' });
reject(new Error('invalid'));
return;
}
toast({ type: 'success', message: `已保存:${name.value}` });
resolve();
}, 900);
});
}
</script>
<template>
<CfButton @click="open = true">编辑配置</CfButton>
<CfDrawer
v-model:open="open"
placement="right"
size="md"
title="编辑配置"
description="保存按钮接异步 onBeforeOk —— 期间所有关闭路径屏蔽。"
ok-text="保存"
cancel-text="取消"
:on-before-ok="onBeforeOk"
@ok="open = false; name = ''"
>
<div style="display: grid; gap: 8px;">
<label style="font-size: 12px; color: var(--fg-3);">名称</label>
<CfInput v-model="name" placeholder="输入≥2字符" />
</div>
</CfDrawer>
</template> 内边缘缩放
resizable 让抽屉的内边缘变成拖拽把手 —— 用户可以横向(左右抽屉)或纵向(上下抽屉)调整尺寸。最小 240×160。
<script setup lang="ts">
import { ref } from 'vue';
import { CfButton, CfDrawer } from '@chufix-design/vue';
const right = ref(false);
const bottom = ref(false);
</script>
<template>
<div style="display: flex; gap: 8px;">
<CfButton @click="right = true">右侧可缩放</CfButton>
<CfButton variant="tertiary" @click="bottom = true">底部可缩放</CfButton>
</div>
<CfDrawer
v-model:open="right"
placement="right"
size="md"
resizable
title="右侧可缩放"
description="抓住面板的左边缘横向拖拽(最小 240px)。"
>
<p style="margin: 0;">这种交互对“调试 / 详情”类抽屉非常实用 —— 用户可以根据自己屏幕宽度自由调整。</p>
</CfDrawer>
<CfDrawer
v-model:open="bottom"
placement="bottom"
size="md"
resizable
title="底部可缩放"
description="抓住面板的上边缘竖向拖拽(最小 160px)。"
>
<p style="margin: 0;">底部抽屉常用于日志、控制台、playground 等可调高度的工具区。</p>
</CfDrawer>
</template> <script setup>
import { ref } from 'vue';
import { CfButton, CfDrawer } from '@chufix-design/vue';
const right = ref(false);
const bottom = ref(false);
</script>
<template>
<div style="display: flex; gap: 8px;">
<CfButton @click="right = true">右侧可缩放</CfButton>
<CfButton variant="tertiary" @click="bottom = true">底部可缩放</CfButton>
</div>
<CfDrawer
v-model:open="right"
placement="right"
size="md"
resizable
title="右侧可缩放"
description="抓住面板的左边缘横向拖拽(最小 240px)。"
>
<p style="margin: 0;">这种交互对“调试 / 详情”类抽屉非常实用 —— 用户可以根据自己屏幕宽度自由调整。</p>
</CfDrawer>
<CfDrawer
v-model:open="bottom"
placement="bottom"
size="md"
resizable
title="底部可缩放"
description="抓住面板的上边缘竖向拖拽(最小 160px)。"
>
<p style="margin: 0;">底部抽屉常用于日志、控制台、playground 等可调高度的工具区。</p>
</CfDrawer>
</template> 不锁背景
默认 mask=true 渲染遮罩并锁定 body 滚动。设置 mask={false} 后抽屉浮在原页面之上,遮罩不渲染、滚动也不锁,适合”参考资料”、“实时日志”这类常驻面板。
<script setup lang="ts">
import { ref } from 'vue';
import { CfButton, CfDrawer } from '@chufix-design/vue';
const open = ref(false);
</script>
<template>
<CfButton @click="open = true">打开(不锁背景)</CfButton>
<p style="margin: 8px 0 0; color: var(--fg-3); font-size: 12px;">
抽屉打开后页面其他地方仍可继续滚动 / 点击 —— 适合“常驻参考面板”型场景。
</p>
<CfDrawer
v-model:open="open"
placement="right"
size="sm"
:mask="false"
title="参考面板"
description="mask=false 时不渲染遮罩、不锁滚动。"
>
<p style="margin: 0;">尝试在抽屉打开的状态下滚动或点击页面其他位置 —— 都能正常交互。</p>
</CfDrawer>
</template> <script setup>
import { ref } from 'vue';
import { CfButton, CfDrawer } from '@chufix-design/vue';
const open = ref(false);
</script>
<template>
<CfButton @click="open = true">打开(不锁背景)</CfButton>
<p style="margin: 8px 0 0; color: var(--fg-3); font-size: 12px;">
抽屉打开后页面其他地方仍可继续滚动 / 点击 —— 适合“常驻参考面板”型场景。
</p>
<CfDrawer
v-model:open="open"
placement="right"
size="sm"
:mask="false"
title="参考面板"
description="mask=false 时不渲染遮罩、不锁滚动。"
>
<p style="margin: 0;">尝试在抽屉打开的状态下滚动或点击页面其他位置 —— 都能正常交互。</p>
</CfDrawer>
</template> 锁死关闭路径
表单 / 长流程里有时不希望误触关闭。三个开关全部置 false 让用户必须通过底部按钮显式提交或取消:
closeOnOverlay={false}—— 点击遮罩不关closeOnEsc={false}—— Esc 不关showClose={false}—— 隐藏右上角 ×
异步态(onBeforeOk 进行中)会自动屏蔽所有关闭路径,不需要手动控制。
<script setup lang="ts">
import { ref } from 'vue';
import { CfButton, CfDrawer } from '@chufix-design/vue';
const open = ref(false);
</script>
<template>
<CfButton @click="open = true">打开(仅按钮可关)</CfButton>
<CfDrawer
v-model:open="open"
title="表单填写中"
:close-on-overlay="false"
:close-on-esc="false"
:show-close="false"
>
<p style="margin: 0;">遮罩 / Esc / 右上角 × 全部禁用 — 用户必须通过底部按钮显式提交或取消。</p>
<template #footer>
<CfButton variant="ghost" @click="open = false">取消</CfButton>
<CfButton @click="open = false">提交</CfButton>
</template>
</CfDrawer>
</template> <script setup>
import { ref } from 'vue';
import { CfButton, CfDrawer } from '@chufix-design/vue';
const open = ref(false);
</script>
<template>
<CfButton @click="open = true">打开(仅按钮可关)</CfButton>
<CfDrawer
v-model:open="open"
title="表单填写中"
:close-on-overlay="false"
:close-on-esc="false"
:show-close="false"
>
<p style="margin: 0;">遮罩 / Esc / 右上角 × 全部禁用 — 用户必须通过底部按钮显式提交或取消。</p>
<template #footer>
<CfButton variant="ghost" @click="open = false">取消</CfButton>
<CfButton @click="open = false">提交</CfButton>
</template>
</CfDrawer>
</template> 命令式服务
drawer.open / confirm / danger 直接弹一个抽屉并返回 Promise<boolean> —— 不需要任何状态绑定。
import { drawer } from '@chufix-design/vue'; // React 端:从 @chufix-design/react 导入
const ok = await drawer.confirm({
title: '设置',
description: '快速调整服务参数',
placement: 'right',
content: SettingsForm, // 字符串 / 组件
onOk: async () => {
await api.save(); // throw 或 return false 阻止关闭
},
});
<script setup lang="ts">
import { CfButton, drawer, toast } from '@chufix-design/vue';
async function openSettings() {
const ok = await drawer.confirm({
title: '设置',
description: '快速调整服务参数 —— 不需要关心 v-model。',
placement: 'right',
size: 'md',
content: '这里通常会嵌入一个 Form 子组件。点击保存以应用更改。',
onOk: async () => {
await new Promise((r) => setTimeout(r, 600));
},
});
if (ok) toast({ type: 'success', message: '设置已保存' });
}
async function deleteFlow() {
const ok = await drawer.danger({
title: '永久删除该工作区?',
description: '所有数据将被清除,且无法恢复。',
placement: 'right',
okText: '我已了解,删除',
cancelText: '保留',
content: '建议先导出工作区作为备份。',
});
toast({
type: ok ? 'error' : 'info',
message: ok ? '工作区已删除' : '已取消删除',
});
}
</script>
<template>
<div style="display: flex; flex-wrap: wrap; gap: 8px;">
<CfButton @click="openSettings">drawer.confirm()</CfButton>
<CfButton variant="danger" @click="deleteFlow">drawer.danger()</CfButton>
</div>
</template> <script setup>
import { CfButton, drawer, toast } from '@chufix-design/vue';
async function openSettings() {
const ok = await drawer.confirm({
title: '设置',
description: '快速调整服务参数 —— 不需要关心 v-model。',
placement: 'right',
size: 'md',
content: '这里通常会嵌入一个 Form 子组件。点击保存以应用更改。',
onOk: async () => {
await new Promise((r) => setTimeout(r, 600));
},
});
if (ok) toast({ type: 'success', message: '设置已保存' });
}
async function deleteFlow() {
const ok = await drawer.danger({
title: '永久删除该工作区?',
description: '所有数据将被清除,且无法恢复。',
placement: 'right',
okText: '我已了解,删除',
cancelText: '保留',
content: '建议先导出工作区作为备份。',
});
toast({
type: ok ? 'error' : 'info',
message: ok ? '工作区已删除' : '已取消删除',
});
}
</script>
<template>
<div style="display: flex; flex-wrap: wrap; gap: 8px;">
<CfButton @click="openSettings">drawer.confirm()</CfButton>
<CfButton variant="danger" @click="deleteFlow">drawer.danger()</CfButton>
</div>
</template> API
| Prop | 类型 | 默认 | 说明 |
|---|---|---|---|
open | boolean | false | 受控开关 |
title | string | — | header 标题 |
description | string | — | 标题下副标题 |
placement | 'left' | 'right' | 'top' | 'bottom' | 'right' | 滑入方向 |
size | 'sm' | 'md' | 'lg' | 'xl' | 'full' | 'md' | 横向→宽度档位;纵向→高度档位 |
tone | 'default' | 'info' | 'success' | 'warning' | 'error' | 'default' | 视觉 tone + 自动图标 |
width | number | string | — | 自定义宽度(左右 placement) |
height | number | string | — | 自定义高度(上下 placement) |
resizable | boolean | false | 内边缘可拖拽缩放 |
mask | boolean | true | 是否渲染遮罩并锁滚动 |
closeOnOverlay | boolean | true | 点遮罩是否关闭 |
closeOnEsc | boolean | true | Esc 是否关闭 |
showClose | boolean | true | 右上角 × |
footerAlign | 'start' | 'center' | 'end' | 'space-between' | 'end' | footer 对齐 |
okText / cancelText | string | — | 默认按钮文案;不传则不渲染默认按钮 |
okVariant | 'primary' | 'danger' | 'secondary' | 'primary' | 确认按钮样式 |
onBeforeOk | () => boolean | void | Promise<...> | — | 异步钩子;false / throw 阻止关闭 |
to | string | Element | 'body' | Teleport / Portal 目标 |
zIndex | number | 自动栈管理 | 自定义 z-index 起点 |
插槽 / 事件
- Vue:
header/ 默认 /footer三个具名插槽。footer插槽接收{ ok, cancel, loading }用于自渲染按钮。 - React:
header、footer(可传 ReactNode 或({ ok, cancel, loading }) => ReactNode)+children。 - 事件:
update:open/close/ok/cancel。
服务方法
| 方法 | tone | 默认按钮 |
|---|---|---|
drawer.open(opts) | 跟 opts 一致 | 跟 opts |
drawer.confirm(opts) | warning | OK + Cancel |
drawer.danger(opts) | error | 红色 OK + Cancel |
反馈与讨论
Drawer 抽屉 的讨论