
amis Drawer 抽屉组件实战指南弹出配置、多级嵌套与事件动作整合【免费下载链接】amis前端低代码框架通过 JSON 配置就能生成各种页面。项目地址: https://gitcode.com/GitHub_Trending/am/amis本文基于 amis前端低代码框架通过 JSON 配置生成页面官方文档中的 Drawer 抽屉组件说明结合开源仓库中的源码实现系统讲解抽屉的打开方式、弹出方向与尺寸控制、蒙层与关闭策略、多级嵌套机制以及 confirm/cancel 事件与 confirm/cancel/setValue 特性动作的完整用法。读完本文你可以用纯 JSON Schema 配置出生产级的侧边栏抽屉并理解其背后的默认值来源、嵌套宽度递减规则与数据流实现。一、Drawer 的定位与渲染链路Drawer 是 amis 的弹出层组件之一用于从屏幕边缘左、右、上、下滑出展示内容典型场景是表单填写、详情查看、多级操作的承载容器。与 Dialog 不同Drawer 的内容面板贴边停靠、占据整侧高度或整行宽度更贴近侧边栏面板的交互形态。从源码结构看一个抽屉的完整生命周期横跨三层动作层DrawerAction 负责响应actionType: drawer的动作请求通过renderer.handleAction把 drawer 配置交给对应组件打开渲染器层Drawer 渲染器 以Renderer({type: drawer, storeType: ModalStore.name, isolateScope: true})注册见 Drawer.tsx#L704-L715负责页脚按钮构建、动作分发、表单数据同步、事件派发等逻辑容器层amis-ui 的 Drawer 容器 基于react-overlays的 Portal 与react-transition-group的 Transition 实现处理 Portal 挂载、500ms 滑入滑出动画、滚条宽度补偿、拖拽调宽等 DOM 细节。抽屉组件的默认值可以在渲染器源码中逐一确认Drawer.tsx#L97-L111position: right、resizable: false、showCloseButton: true、overlay: true、closeOnEsc: false、closeOnOutside: false与下文属性表一致。二、基本用法按钮 actionType: drawer最直接的打开方式是按钮配置actionType: drawer并在drawer字段中内联声明抽屉 Schema{ label: 弹出, type: button, actionType: drawer, drawer: { title: 抽屉标题, body: 这是一个抽屉 } }drawer字段的内容就是一个标准的 amis 子 Schematitle是弹出层标题body是内容区两者都可以是任意 SchemaNode支持表达式与数据映射。三、多级弹框嵌套抽屉抽屉支持嵌套使用——在抽屉内部再触发actionType: drawer即可弹出下一层{ type: button, label: 多级抽屉, actionType: drawer, drawer: { title: 提示, body: 这是个简单的抽屉, actions: [ { type: button, actionType: confirm, label: 确认, primary: true }, { type: button, actionType: drawer, label: 再弹一个, drawer: { title: 抽屉中的抽屉, body: 如果你想可以继续弹下去, actions: [ { type: button, actionType: drawer, label: 来吧, level: info, drawer: { title: 抽屉中的抽屉, body: 如果你想可以无限弹下去, actions: [ { type: button, actionType: confirm, label: 不弹了, primary: true } ] } } ] } } ] } }嵌套抽屉有两个由源码保证的行为细节官方文档未展开这里补充宽度逐层递减嵌套抽屉不会完全盖住外层而是每深入一层宽度递减 20px最多处理到第 6 层。实现位于 _drawer.scss#L184-L240样式通过amisModal--Nth层级类名匹配calc(var(--Drawer-widthMd) - (N-1) * 20px)从而形成可视的层叠阶梯效果。外层不会被子层误关渲染器中handleSelfClose会先检查store.dialogOpen与store.drawerOpenDrawer.tsx#L186-L192只要还有子层弹框开着外层就拒绝关闭closeOnOutside也在渲染时做了同样防护Drawer.tsx#L547-L549只有当没有更上层的 drawer/dialog 打开时才生效。四、指定弹出方向 position通过position指定抽屉弹出方向可选left、right、top、bottom默认right{ type: button-toolbar, buttons: [ { type: button, label: 左侧弹出, actionType: drawer, drawer: { position: left, title: 提示, body: 这是一个从左侧弹出的抽屉 } }, { type: button, label: 右侧弹出, actionType: drawer, drawer: { position: right, title: 提示, body: 这是一个从右侧弹出的抽屉 } }, { type: button, label: 顶部弹出, actionType: drawer, drawer: { position: top, title: 提示, body: 这是一个从顶部弹出的抽屉 } }, { type: button, label: 底部弹出, actionType: drawer, drawer: { position: bottom, title: 提示, body: 这是一个从底部弹出的抽屉 } } ] }容器层通过给根节点添加amisDrawer--{position}类名切换定位与滑动方向例如右向抽屉的内容面板初始transform: translate3d(100%, 0, 0)进入时归零_drawer.scss#L281-L328。top/bottom抽屉横向拉满left/right抽屉纵向拉满。五、预设尺寸 size通过size控制抽屉大小可选xs、sm、md、lg、xl{ type: button-toolbar, buttons: [ { type: button, label: 极小框, actionType: drawer, drawer: { size: xs, title: 提示, body: 这是一个极小的抽屉 } }, { type: button, label: 小框, actionType: drawer, drawer: { size: sm, title: 提示, body: 这是一个小抽屉 } }, { type: button, label: 中框, actionType: drawer, drawer: { size: md, title: 提示, body: 这是一个标准的抽屉 } }, { type: button, label: 大框, actionType: drawer, drawer: { size: lg, title: 提示, body: 这是一个大抽屉 } }, { type: button, label: 超大框, actionType: drawer, drawer: { size: xl, title: 提示, body: 这是一个超大的抽屉 } } ] }这些尺寸档位的具体数值来自默认主题的 CSS 变量_components.scss#L1858-L1862size宽度/高度默认值对应 CSS 变量xs200px--drawer-size-xs-widthsm300px--drawer-size-sm-widthmd500px--drawer-size-md-widthlg800px--drawer-size-lg-widthxl90%视口宽度的 90%--drawer-size-xl-width由于走的是 CSS 变量接入方可以通过主题定制覆盖这些档位不设置size时使用默认档属性表中width默认 500px 即对应md档。size对left/right方向控制宽度、对top/bottom方向控制高度_drawer.scss#L152-L182。六、自定义尺寸 width / height除了五档预设还可以直接给像素或 CSS 值。值如果是数字类型单位默认px如果是字符串类型可以使用%、vw、px等任意 CSS 长度width在position为left或right时生效height在position为top或bottom时生效。{ type: button-toolbar, buttons: [ { type: button, label: 自定义宽度, actionType: drawer, drawer: { position: right, width: 300, title: 提示, body: 这是一个自定义 300px 宽度的抽屉 } }, { type: button, label: 自定义高度, actionType: drawer, drawer: { position: bottom, height: 300, title: 提示, body: 这是一个自定义 300px 高度的抽屉 } } ] }源码中该逻辑集中在getDrawerStyleDrawer.tsx#L211-L226只有方向与维度匹配时width/height才会作为内联样式写到内容面板上因此方向不匹配的尺寸配置会被静默忽略。七、可拖拽调整大小 resizable配置resizable: true后抽屉边缘会出现一个带···标识的拖拽手柄Drawer-resizeCtrl节点Drawer.tsx#L296-L308按住拖动即可实时改变抽屉尺寸{ type: button, label: 可拖拽调整大小, actionType: drawer, drawer: { resizable: true, title: 提示, body: 这是一个可拖拽的抽屉 } }实现细节Drawer.tsx#L228-L294mousedown时记录初始偏移并在document.body上临时挂载mousemove/mouseup监听mousemove时按当前方向实时改写内容面板的width或height有最大尺寸保护左右/上下方向的最大值限制为calc(100% - 50px)避免拖满后关闭按钮不可点拖拽期间通过onDragging回调同步store.inDragging状态渲染器会把它下发给 body 内组件如表格用于在拖拽中禁用干扰交互。八、关闭按钮、蒙层与关闭方式隐藏关闭按钮 showCloseButton{ type: button, label: 无关闭按钮, actionType: drawer, drawer: { title: 提示, body: 这是一个没有关闭按钮的抽屉, showCloseButton: false } }按属性表说明showCloseButton为false时默认开启closeOnOutside点击外部关闭适合作为必须显式操作才能退出的场景兜底。蒙层 overlay通过overlay控制是否显示蒙层默认true{ type: button-toolbar, buttons: [ { type: button, label: 默认, actionType: drawer, drawer: { overlay: true, title: 提示, body: 这是一个有蒙层的抽屉 } }, { type: button, label: 不显示, actionType: drawer, drawer: { overlay: false, title: 提示, body: 这是一个没有蒙层的抽屉 } } ] }源码层面overlay: false时根节点会加上amisDrawer--noOverlay类对应样式pointer-events: none_drawer.scss#L136-L138——即无蒙层的抽屉不会拦截背景页面的鼠标事件页面其余部分仍可正常操作。closeOnOutside 与 closeOnEsc{ type: button-toolbar, buttons: [ { type: button, label: 点击抽屉外自动关闭有蒙层, actionType: drawer, drawer: { closeOnOutside: true, title: 提示, body: 这是一个简单的抽屉 } }, { type: button, label: 点击抽屉外自动关闭无蒙层, actionType: drawer, drawer: { overlay: false, closeOnOutside: true, title: 提示, body: 这是一个简单的抽屉 } }, { type: button, label: 按键关闭, actionType: drawer, drawer: { closeOnEsc: true, title: 提示, body: 试试按下 Esc } } ] }closeOnOutside的实现值得注意容器在抽屉进入后于document.body上以捕获阶段监听mousedown/mouseupDrawer.tsx#L119-L135、L173-L209只有当按下和抬起两次都发生在抽屉外部时才触发关闭因此从抽屉内拖选文本拖到外部松手这类误触不会导致关闭。无蒙层模式下还专门排除了对[roledialog]弹层内部点击的误判。九、动作完成后关闭抽屉close 属性在抽屉中配置行为按钮如actionType: ajax可以在按钮上配置close: true在该动作请求完成后自动关闭当前抽屉{ type: button, label: 打开, actionType: drawer, drawer: { title: 提示, body: [ { type: button-toolbar, buttons: [ { type: button, label: 默认的 ajax 请求, actionType: ajax, api: /api/mock2/form/saveForm?waitSeconds1 }, { type: button, label: ajax 请求成功后关闭抽屉, actionType: ajax, api: /api/mock2/form/saveForm?waitSeconds1, close: true } ] } ] } }关闭指定的上层抽屉3.3.0 及以上版本如果希望关闭更外层的抽屉给目标抽屉设置name属性然后把按钮的close配置为该name{ type: button, label: 多级抽屉, actionType: drawer, drawer: { title: 提示, body: 这是个简单的抽屉, name: drawer_1, actions: [ { type: button, actionType: confirm, label: 确认, primary: true }, { type: button, actionType: drawer, label: 再弹一个, drawer: { title: 抽屉中的抽屉, body: 关闭当前抽屉时将外层抽屉一并关闭, actions: [ { type: button, label: 关闭所有, level: info, close: drawer_1 } ] } } ] } }对应源码路径在DrawerRenderer.handleActionaction.close true时调用handleSelfClose()关闭自身否则调用closeTarget(action.close)其实现是scoped.close(target)Drawer.tsx#L1046-L1049即按name在作用域内定位目标抽屉并关闭因此一次点击可以把外层抽屉一并收掉。表单上禁用自动关闭close: false如果抽屉 body 里放的是表单提交成功后通常会自动关闭抽屉可以在表单上配置close: false保持抽屉打开{ type: button, label: 弹个框, actionType: drawer, drawer: { type: form, api: /api/mock2/form/saveForm, body: [ { type: input-text, name: name, label: 姓名 } ], close: false } }从源码看渲染器在handleChildFinished中判断action.close false时直接返回不触发onConfirm关闭流程Drawer.tsx#L964-L967。这使提交成功但抽屉不关、由用户手动关闭或继续操作成为可能。十、自定义页脚按钮 actions不设置actions时抽屉自动生成取消 确认两个按钮buildActions实现Drawer.tsx#L158-L184先 push 一个actionType: close的取消按钮再在confirm为真时 push 主色确认按钮一旦通过actions显式配置则以配置为准{ type: button-toolbar, buttons: [ { type: button, label: 无按钮, actionType: drawer, drawer: { title: 提示, body: 这是一个没有按钮的抽屉, actions: [] } }, { type: button, label: 一个按钮, actionType: drawer, drawer: { title: 提示, body: 只有一个 OK 的抽屉, actions: [ { type: button, actionType: confirm, label: OK, primary: true } ] } } ] }actions: []空数组会直接隐藏整个页脚renderFooter中!actions.length时返回null。十一、属性表完整属性名类型默认值说明typestringdrawer指定为 Drawer 渲染器titleSchemaNode弹出层标题bodySchemaNode往 Drawer 内容区加内容sizestring指定 Drawer 大小支持xs、sm、md、lg、xlpositionstringright指定 Drawer 方向支持left、right、top、bottomclassNamestringDrawer 最外层容器的样式类名headerClassNamestringDrawer 头部区域的样式类名bodyClassNamestringmodal-bodyDrawer body 区域的样式类名footerClassNamestringDrawer 页脚区域的样式类名showCloseButtonbooleantrue是否展示关闭按钮当值为false时默认开启closeOnOutsidecloseOnEscbooleanfalse是否支持按 Esc 关闭 DrawercloseOnOutsidebooleanfalse点击内容区外是否关闭 Draweroverlaybooleantrue是否显示蒙层resizablebooleanfalse是否可通过拖拽改变 Drawer 大小widthstring \| number500px容器的宽度在position为left或right时生效heightstring \| number500px容器的高度在position为top或bottom时生效actionsArray【确认】和【取消】可以不设置默认只有两个按钮dataobject支持数据映射如果不设定将默认将触发按钮的上下文中继承数据关于data属性动作层的证据是 DrawerAction.ts#L45-L65打开抽屉时默认透传data: action.rawData即触发按钮事件时的事件数据会进入抽屉数据域无需显式配置。十二、事件表confirm 与 cancel抽屉对外派发confirm与cancel两个事件可通过onEvent监听并在动作中用${事件参数名}或${event.data.[事件参数名]}取值机制详见事件动作事件名称事件参数说明confirmevent.data: object抽屉数据[name]: any当前数据域中指定字段的值点击确认提交时触发cancelevent.data: object抽屉数据[name]: any当前数据域中指定字段的值点击取消时触发触发时机会在渲染器源码中得到印证handleAction中close/cancel分支会先dispatchEvent(cancel, ...)confirm分支先dispatchEvent(confirm, ...)Drawer.tsx#L851-L881任一监听动作调用了preventDefault即可阻止默认关闭行为——事件不只是通知还能拦截。confirm 事件示例[ { label: 打开, type: button, onEvent: { click: { actions: [ { actionType: drawer, drawer: { title: 标题, body: 这是一个抽屉, onEvent: { confirm: { actions: [ { actionType: toast, args: { msg: confirm } } ] } } } } ] } } } ]cancel 事件示例[ { label: 打开, type: button, onEvent: { click: { actions: [ { actionType: drawer, drawer: { title: 标题, body: 这是一个抽屉, onEvent: { cancel: { actions: [ { actionType: toast, args: { msg: cancel } } ] } } } } ] } } } ]十三、动作表confirm / cancel / setValue其他组件可通过actionType: 动作名称componentId: 组件id远程驱动一个已打开的抽屉参数经args: {配置项: xxx}传递机制详见事件动作动作名称动作配置说明confirm-确认提交cancel-取消关闭setValuevalue: object更新的数据更新数据confirm 动作给抽屉设置id其他组件即可用actionType: confirmcomponentId触发确认等价于点击确认按钮会执行 body 内表单/组件的提交逻辑{ type: button, label: 弹个表单, actionType: drawer, drawer: { title: 在抽屉中的表单, id: drawer_confirm, body: { type: form, api: /api/mock2/form/saveForm?waitSeconds2, body: [ { type: input-text, name: username, required: true, placeholder: 请输入用户名, label: 用户名 }, { type: input-password, name: password, label: 密码, required: true, placeholder: 请输入密码 }, { type: checkbox, name: rememberMe, label: 记住登录 } ] }, actions: [ { type: button, label: 触发确认, onEvent: { click: { actions: [ { actionType: confirm, componentId: drawer_confirm } ] } } } ] } }cancel 动作{ type: button, label: 弹个表单, actionType: drawer, drawer: { title: 在抽屉中的表单, id: drawer_cancel, body: { type: form, api: /api/mock2/form/saveForm?waitSeconds2, body: [ { type: input-text, name: username, required: true, placeholder: 请输入用户名, label: 用户名 }, { type: input-password, name: password, label: 密码, required: true, placeholder: 请输入密码 }, { type: checkbox, name: rememberMe, label: 记住登录 } ] }, actions: [ { type: button, label: 触发取消, onEvent: { click: { actions: [ { actionType: cancel, componentId: drawer_cancel } ] } } } ] } }setValue 动作合并与覆盖两种模式合并数据默认默认setValue会将新数据与目标组件数据合并{ type: button, label: 弹个表单, actionType: drawer, drawer: { title: 在抽屉中的表单, id: drawer_setvalue, data: { username: amis, password: amisbaidu.com }, body: [ { type: alert, body: 初始化时抽屉的数据 data 为 {username: amis, password: fex}表单内或者表单外都可以读取这些数据当点击【更新抽屉数据】按钮后抽屉的数据被更新为 {username: aisuda, password: aisudabaidu.com} }, { type: input-text, label: 表单外的密码, name: password }, { type: form, debug: true, api: /api/mock2/form/saveForm?waitSeconds2, body: [ { type: input-text, name: username, required: true, placeholder: 请输入用户名, label: 用户名 }, { type: input-password, name: password, label: 密码, required: true, placeholder: 请输入密码 } ] } ], actions: [ { type: button, label: 更新抽屉数据, onEvent: { click: { actions: [ { actionType: setValue, componentId: drawer_setvalue, args: { value: { username: aisuda, password: aisudabaidu.com } } } ] } } } ] } }覆盖数据通过dataMergeMode: override可以整体覆盖目标组件数据原有字段被删除{ type: button, label: 弹个表单, actionType: drawer, drawer: { title: 在抽屉中的表单, id: drawer_setvalue2, data: { username: amis, password: amisbaidu.com }, body: [ { type: alert, body: 初始化时抽屉的数据 data 为 {username: amis, password: fex}表单内或者表单外都可以读取这些数据当点击【更新抽屉数据】按钮后抽屉的数据被更新为 {username: aisuda}即 password 将被删除 }, { type: input-text, label: 表单外的密码, name: password }, { type: form, debug: true, api: /api/mock2/form/saveForm?waitSeconds2, body: [ { type: input-text, name: username, required: true, placeholder: 请输入用户名, label: 用户名 }, { type: input-password, name: password, label: 密码, required: true, placeholder: 请输入密码 } ] } ], actions: [ { type: button, label: 更新抽屉数据, onEvent: { click: { actions: [ { actionType: setValue, componentId: drawer_setvalue2, args: { value: { username: aisuda } }, dataMergeMode: override } ] } } } ] } }从源码看setValue最终落到渲染器的setData(values, replace)→store.updateData(values, undefined, replace)Drawer.tsx#L1051-L1053dataMergeMode未指定时执行深合并为override时以新数据整体替换。十四、进阶等待抽屉结果并回填变量官方文档未展开、但源码中明确支持的一个能力在actionType: drawer的动作上配置waitForAction: true动作会等待抽屉关闭并拿到{confirmed, value}结果随后通过outputVar默认drawerResponse写入事件数据域DrawerAction.ts#L18-L80{ type: button, label: 打开抽屉并等待结果, actionType: drawer, waitForAction: true, outputVar: drawerResult, drawer: { title: 标题, body: 这是一个抽屉 }, onEvent: { click: { actions: [ { actionType: toast, args: { msgType: success, msg: 确认状态${drawerResult.confirmed} } } ] } } }这使得抽屉即弹窗回调的模式成为可能后续动作可以直接引用用户是否确认以及抽屉数据域的值而无需依赖全局数据同步。十五、小结与延伸阅读Drawer 组件的能力边界可以用一句话概括它既是纯声明式的 UI 容器position/size/overlay/resizable 等展示属性又是具备完整动作协议的业务容器confirm/cancel 事件、远程 confirm/cancel/setValue 动作、close 关闭编排、waitForAction 结果回填。关键要点回顾默认右侧弹出、md档500px宽度、带蒙层、带取消确认页脚按钮均可由属性表覆盖嵌套抽屉每层自动缩进 20px且外层在子层未关闭前拒绝外部点击关闭close: true/close: drawerName/ 表单close: false构成三级关闭控制width/height与position存在方向匹配关系方向不匹配时配置被忽略尺寸档位走 CSS 变量可通过主题定制统一调整。进一步可在仓库中查阅的材料可运行示例examples/components/Dialog/Drawer.jsx对应文档站 Drawer 示例页渲染器实现packages/amis/src/renderers/Drawer.tsxUI 容器实现packages/amis-ui/src/components/Drawer.tsx样式实现packages/amis-ui/scss/components/_drawer.scss动作实现packages/amis-core/src/actions/DrawerAction.ts测试用例packages/amis/tests/event-action/drawer.test.tsx。【免费下载链接】amis前端低代码框架通过 JSON 配置就能生成各种页面。项目地址: https://gitcode.com/GitHub_Trending/am/amis创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考