☰
cube-ui Drawer 抽屉组件实战指南:从级联选择到自定义 Panel 的完整实现
2026/9/25 2:32:21 网站建设 项目流程
  • 前端
  • UI组件
  • 移动开发

【免费下载链接】cube-ui

:large_orange_diamond: A fantastic mobile ui lib implement by Vue

项目地址:https://gitcode.com/gh_mirrors/cu/cube-ui
点击查看免费下载

Drawer(抽屉)是 cube-ui(仓库根目录)中一个从侧边滑出的满屏级联选择容器,在省市区联动、多级分类等“大范围层级选择”场景中非常实用。本文将基于 Drawer 官方文档 结合组件源码,系统讲解其 Props、事件、实例方法,并通过默认配置与自定义插槽两套完整示例,带你掌握从数据驱动到异步级联的完整开发能力。

一、Drawer 是什么:定位与适用场景

Drawer 是 cube-ui 在1.7.0版本新增的组件,文档将其定位为“抽屉”,主要用于需要大范围层级进行选择的场景,一般情况下应该是满屏状态。与弹出层(Popup)不同,Drawer 的交互模型是:

  • 从屏幕右侧滑出主面板,默认max-width: 90%;
  • 内部按列(Panel)组织数据,每列宽约 170px,多列之间以层叠(margin-left: -67px)的方式排布,实现“抽屉拉开逐层选择”的视觉效果;
  • 点击某一列中的条目后,下一列自动填充并滑出,直到选中最后一个 Panel 中的项完成整个选择。

关键依赖(务必记住):__注:组件依赖父容器相对定位或者绝对定位,因为 Drawer 是绝对定位的。从 drawer.vue 的样式可以看到.cube-drawer使用position: absolute; top/right/bottom/left: 0铺满父容器,因此父容器必须设置position: relative或position: absolute,否则 Drawer 会定位到最近的定位祖先甚至视口,造成布局错乱。参考官方示例 example/pages/drawer/default.vue,其.view-wrapper使用position: fixed即为此目的。

组件由三部分组成,对应三个子组件:

| 子组件 | 职责 | | - | - | |cube-drawer| 整体容器:负责标题、Panel 的编排、滑出动画与事件分发 | |cube-drawer-panel| 单列面板:内嵌cube-scroll实现滚动 | |cube-drawer-item| 面板中的单个选项行 |

三者在 src/modules/drawer/index.js 中一并注册,因此Vue.use(Drawer)后即可同时使用这三个组件;模块同时暴露Drawer.Panel、Drawer.Item便于按需引用。

二、默认配置使用:一个省市区联动 Demo

文档给出了最典型的“默认配置使用”示例,下面的代码与 example/pages/drawer/default.vue 完全一致,可直接在项目中复制运行:

<cube-button @click="showDrawer">Show Drawer</cube-button> <cube-drawer ref="drawer" title="请选择" :data="data" :selected-index="selectedIndex" @change="changeHandler" @select="selectHandler" @cancel="cancelHandler"></cube-drawer>
import { provinceList, cityList, areaList } from '../../data/area' export default { data() { return { selectedIndex: [], data: [ provinceList, [], [] ] } }, methods: { showDrawer() { this.$refs.drawer.show() }, changeHandler(index, item, selectedVal, selectedIndex, selectedText) { // fake request setTimeout(() => { let data if (index === 0) { // procince change, get city data data = cityList[item.value] } else { // city change, get area data data = areaList[item.value] } // refill panel(index + 1) data this.$refs.drawer.refill(index + 1, data) }, 200) }, selectHandler(selectedVal, selectedIndex, selectedText) { this.$createDialog({ type: 'warn', content: `Selected Item: <br/> - value: ${selectedVal.join(', ')} <br/> - index: ${selectedIndex.join(', ')} <br/> - text: ${selectedText.join(' ')}`, icon: 'cubeic-alert' }).show() }, cancelHandler() { console.log('cancel') } } }

关键点逐一解读

  • title就是标题,可选:不传则标题栏不渲染(源码中通过v-show="$slots.title || title"控制,见 drawer.vue)。
  • data数据源,二维数组,长度决定了抽屉的 Panel 数,初始长度一定要确定:外层数组的每一项对应一列 Panel。上例中[provinceList, [], []]表示三列(省/市/区),后两列初始为空,等待联动填充。初始长度必须确定是硬性要求——show()时组件会按data.length逐列推进并计算位移(见 drawer.vue),若初始长度不足会导致后续列无法展示。
  • selected-index是初始选择的索引值:传入数组,例如[0]表示打开抽屉后第一列默认选中第 0 项。源码中通过selected: [...this.selectedIndex]拷贝并响应selectedIndex变化(drawer.vue)。
  • 三个事件:
    • change:选择发生改变,即选中非最后一个 Panel 中的项时触发;
    • select:选中最后一个 Panel 中的项,完成最终选择后触发;
    • cancel:点击左侧空白蒙层触发。

异步级联的核心:refill方法

文档明确指出:“你可以在change中通过 Drawer 的refill方法更新下一个 Panel 的数据,可以是同步更新也可以是异步更新。”上例中用setTimeout(..., 200)模拟网络请求,拿到数据后调用this.$refs.drawer.refill(index + 1, data)填充下一列。从源码看,refill做了三件事(drawer.vue):

  1. this.$set(this.data, panelIndex, data)——响应式替换对应列的数据;
  2. 截断已选中的索引、值、文本集合(slice(0, panelIndex)),把层级停留在当前列;
  3. 若传入第三个参数index(默认选中项,文档建议不填),则同步触发选中逻辑。

三、自定义使用:插槽 + 子组件定制面板

“你可以通过插槽来自定义结构。”当默认样式无法满足需求时,可在cube-drawer内使用cube-drawer-panel与cube-drawer-item自由组合。以下代码与 example/pages/drawer/custom.vue 一致:

<cube-drawer ref="drawer" :data="data" :selected-index="selectedIndex" @change="changeHandler" @select="selectHandler" @cancel="cancelHandler"> <span slot="title">{{province.text}}</span> <cube-drawer-panel v-for="(panel, index) in data" :key="index" :index="index" :data="panel" > <cube-drawer-item v-for="(item, i) in panel" :item="item" :key="i" :index="i"> <i class="cubeic-round-border"></i> <span>{{item.text}}</span> </cube-drawer-item> </cube-drawer-panel> </cube-drawer>
import { provinceList, cityList, areaList } from '../../data/area' export default { data() { return { province: {}, selectedIndex: [], data: [ [], [] ] } }, methods: { showDrawer() { // get radom province const randomIndex = Math.round(Math.random() * provinceList.length) const randomProvince = provinceList[randomIndex] this.province = randomProvince this.$refs.drawer.refill(0, cityList[randomProvince.value]) this.$refs.drawer.show() }, changeHandler(index, item, selectedVal, selectedIndex, selectedText) { setTimeout(() => { // city change, get area data const data = areaList[item.value] this.$refs.drawer.refill(index + 1, data) }, 200) }, selectHandler(selectedVal, selectedIndex, selectedText) { this.$createDialog({ type: 'warn', content: `Selected Item: <br/> - value: ${selectedVal.join(', ')} <br/> - index: ${selectedIndex.join(', ')} <br/> - text: ${selectedText.join(' ')}`, icon: 'cubeic-alert' }).show() }, cancelHandler() { console.log('cancel') } } }

自定义使用要点

  • slot="title"自定义标题:可注入任意内容(上例为随机省份名),未提供插槽时回退为titleprop。
  • cube-drawer-panel接收index(在 data 中的位置)与data(该列数据),面板内默认使用cube-scroll包裹,列表可滚动。
  • cube-drawer-item接收item(数据项)与index(在面板内的位置),其默认插槽渲染{{item.text || item}},自定义时可注入图标(如cubeic-round-border)等任意内容。
  • 自定义时的联动填充:此例在showDrawer里先随机选一个省份,refill(0, cityList[...])填充第一列后再show(),与默认示例的“先 show 后联动”路径不同,但同样合法。

值得注意的是,若自定义面板需要覆盖选中高亮样式,可像 custom.vue 一样为.cube-drawer-item_active追加样式(默认高亮色来自主题变量$drawer-item-active-bgc)。

四、Props 配置详解

CubeDrawer

| 参数 | 说明 | 类型 | 可选值 | 默认值 | | - | - | - | - | - | | title | 标题 | String | - | '' | | data | 数据源 | Array | - | [] | | selectedIndex | 初始选择索引 | Array | - | [] | | visible1.8.1| 显示状态,是否可见。v-model绑定值 | Boolean | true/false | false |

其中visible是1.8.1新增的受控 prop,通过v-model双向绑定控制显隐。其实现来自 visibility.js 混入:组件内部维护isVisible数据并监听visible的变化(为true时调用show()),同时通过toggle事件把内部状态同步出去,从而支持v-model="visible"这种写法。注意show()/hide()与visible是同一套状态的两种控制入口,混用时要避免状态不一致。

此外,Drawer 还混入了 popup.js(提供zIndex、maskClosable两个可选 prop),但组件自身的显隐主要依赖 visibilityMixin 与isVisible。

data子配置项

data是一个二维数组,数组中每一项仍然为数组,结构类似于:

[ [ { text: 'text', value: 'value' }, ... ], [ 'text', 'text2', ... ] ]

里层数组的每一项可以是对象(包含 text 和 value),也可以是纯字符串。从 drawer.vue 的changeHandler可以看到,当数据项为字符串时,selectedVal与selectedText都取该字符串本身;当为对象时,值集合取item.value、文本集合取item.text。这也是select回调中selectedText拼接展示“省 市 区”的底层依据。

CubeDrawerPanel

| 参数 | 说明 | 类型 | 可选值 | 默认值 | | - | - | - | - | - | | data | 数据源 | Array | - | [] | | index | 该数据源在 CubeDrawer 的 data 中的索引值 | Number | - | -1 |

CubeDrawerItem

| 参数 | 说明 | 类型 | 可选值 | 默认值 | | - | - | - | - | - | | data | 数据项 | String/Object | - | '' | | index | 该数据项在 CubeDrawerPanel 的 data 中的索引值 | Number | - | -1 |

说明:文档 Props 表中 CubeDrawerItem 的写法为data,对应源码 drawer-item.vue 中实际 prop 名为item(类型[String, Object])。使用自定义插槽结构时,请以源码实际 prop 名item为准传入数据项,例如<cube-drawer-item :item="item" :index="i">。

五、事件详解

| 事件名 | 说明 | 参数1 | 参数2 | 参数3 | 参数4 | 参数5 | | - | - | - | - | - | - | - | | change | 选择发生改变(选中是非最后一个 Panel 中的项的时候触发) | 发生改变的 Panel 的索引 | 发生改变的数据项 | 已选中的值集合 | 已选中的索引集合 | 已选中的文本集合 | | select | 选择了最后一个 Panel 中的项触发 | 已选中的值集合 | 已选中的索引集合 | 已选中的文本集合 | - | - | | cancel | 点击左侧空白区域触发 | - | - | - | - | - |

从源码看,三个事件的分发集中在 drawer.vue:

  • changeHandler(panelIndex, item, index)在不是最后一列时$emit('change', ...),供业务方异步拉取下一列数据;
  • 当panelIndex === data.length - 1时$emit('select', ...)并自动调用hide()——即选中最后一个 Panel 的项后抽屉自动收起,无需手动关闭;
  • drawerClick(点击.cube-drawer空白蒙层)时hide()并$emit('cancel')。

六、实例方法

| 方法名 | 说明 | 参数1 | 参数2 | 参数3 | | - | - | - | - | - | | refill | 填充数据,改变某个 Panel 数据 | 要改变的 Panel 的索引 | 填充数据 | 默认选中项(可选,建议不填) | | show | 显示 | - | - | - | | hide | 隐藏 | - | - | - |

  • refill(panelIndex, data, index?):核心联动方法。前两个参数必填;第三个参数为默认选中项,文档明确“建议不填”,因为默认行为会保留当前选中并仅更新数据,传入 index 会额外触发一次选中变更,一般用于初始化。
  • show():显示抽屉。源码中会按data.length遍历 Panel,若某列缺少选中项且不是第一列,会以当前已选索引触发一次changeHandler以推进级联(drawer.vue)。
  • hide():隐藏抽屉。通过修改translate3d位移触发过渡,transitionend后才真正把isVisible置为false(drawer.vue)。

对应的 TypeScript 声明位于 types/components/Drawer.d.ts:refill: (index?: number, data?: any[], item?: any[]) => void、show()、hide()。

七、源码原理与测试验证

层级切换与位移计算

Drawer 的多列展示并非“并排平铺”,而是通过translate3d负位移让当前列滑到最右侧可点区域。核心逻辑在computedStyle()(drawer.vue):遍历当前index之前的所有 Panel,累加其offsetWidth与 margin,再对.cube-drawer-main设置translate3d(-allWidth, 0, 0),配合transition: transform .3s ease-in-out产生平滑的抽拉动画。Panel 间默认margin-left: -67px、进入/离开动画位移 67px(drawer-panel.vue),形成层叠抽屉的视觉层次。

滚动与联动细节

每个cube-drawer-panel内部用cube-scroll承载列表,并监听data变化回滚到顶部(scrollToTop),监听isVisible变化后refresh()以重算滚动区域(drawer-panel.vue);Panel 在mounted时通过$parent.addPanel(this)注册到 Drawer,beforeDestroy时注销,保证位移计算始终基于真实 DOM。

单元测试佐证

组件行为在 test/unit/specs/drawer.spec.js 中有完整覆盖,可作为行为规范参考:

  • 渲染与联动:构造data: [['1', '2'], [], []]三列数据,断言标题渲染、Panel 数量为 3、初始隐藏(display: none);show()后第一列显示且首项高亮(含cube-drawer-item_activeclass);模拟change回调中refill(index + 1, [...])后,第二列、第三列依次填充并显示。
  • 事件触发顺序:点击非末列条目仅触发change;点到最后列条目触发select并自动隐藏;点击空白蒙层触发cancel。
  • 回退联动:选中第一列后回点第一列其他项,第二列数据被重新填充。

测试代码中的交互模式(dispatchTap点击选项、refill异步填充)与文档示例完全一致,可直接对照验证业务写法。

八、实践注意事项

  1. 父容器定位:Drawer 是绝对定位,父容器必须是相对/绝对定位(如position: fixed的.view-wrapper),否则抽屉位置错乱。
  2. data初始长度必须确定:二维数组外层长度决定 Panel 数量,联动只负责“填充内容”而非“新增列”;需要更多层级时,初始化就要预留对应长度的空数组。
  3. 数据项两种形态:对象{ text, value }或纯字符串均可混用于不同列,但注意对象形态下selectedVal取value、selectedText取text。
  4. 异步填充放change:在change回调里refill(index + 1, data),同步或异步(如setTimeout/请求返回后)都支持。
  5. visible与 v-model(1.8.1+):需要外部控制显隐时用v-model绑定visible;方法调用与 v-model 混用需注意状态同步。
  6. 选中末列自动收起:select触发后组件自动hide(),无需手动关闭;若需要选择后留在抽屉内做额外操作,可自行结合refill与selectedIndex设计。
  7. 按需引入:通过import Drawer from 'cube-ui/lib/drawer'(对应 lib/drawer 构建产物)或全局Vue.use(Drawer)均可;Drawer 依赖cube-scroll,引入时需确保 scroll 组件可用。

通过本文的示例与源码对照,你可以直接在业务中落地 Drawer 的级联选择能力——从简单的两三级联动,到结合自定义插槽打造完全符合设计稿的多列抽屉,均能从容实现。

  • 前端
  • UI组件
  • 移动开发

【免费下载链接】cube-ui

:large_orange_diamond: A fantastic mobile ui lib implement by Vue

项目地址:https://gitcode.com/gh_mirrors/cu/cube-ui
点击查看免费下载
上一篇:Vercel 上的 React Router v7 Single-Fetch `.data` 路由:端到端 fixture 实战与源码解析
下一篇:国家中小学智慧教育平台电子课本下载工具:Python技术实现深度解析

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询