尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Vant PickerGroup 选择器组实战:一次交互完成多个 Picker 的联动选择

Vant PickerGroup 选择器组实战:一次交互完成多个 Picker 的联动选择 Vant PickerGroup 选择器组实战一次交互完成多个 Picker 的联动选择【免费下载链接】vantA lightweight, customizable Vue UI library for mobile web apps.项目地址: https://gitcode.com/GitHub_Trending/va/vantPickerGroup选择器组是 Vant 移动端 UI 库中用于组合多个 Picker 类组件、在一次交互中完成多个值选择的容器组件。它特别适合预约日期时间开始/结束日期范围开始/结束时间范围这类需要连续选择多个值的表单场景。读完本文你将掌握 PickerGroup 的组件组合规则、统一工具栏与下一步分步引导、受控/非受控模式切换以及如何基于源码理解其父子通信的实现原理。什么是 PickerGroup在移动端表单中经常遇到需要在一个弹层里连续选择多个值的场景例如同时选择日期和时间预约、提醒选择开始日期到结束日期的日期范围选择开始时间到结束时间的时间范围。如果直接堆叠多个独立的 Picker需要自己处理多个标题栏、多个确认按钮、以及多个值的回传逻辑体验割裂且代码繁琐。PickerGroup正是为解决这一问题而生它提供一个统一的容器内部通过Tabs 标签页组织多个选择器并代子组件渲染统一的标题栏让用户在同一个界面里按步骤完成全部选择。支持放入的组件PickerGroup的默认插槽中可以放置以下组件Picker基础选择器Area省市区选择器DatePicker日期选择器TimePicker时间选择器其他基于 Picker 封装的自定义组件。从源码看这种可扩展能力来自基于vant/use的父子注入机制PickerGroup在 PickerGroup.tsx 中通过useChildren(PICKER_GROUP_KEY)收集所有子选择器并建立关联而 Picker.tsx 中通过useParent(PICKER_GROUP_KEY)感知自己处于选择器组内部。这意味着只要是内部调用了useParent(PICKER_GROUP_KEY)、并暴露统一confirm()接口的 Picker 衍生组件都可以直接放进 PickerGroup。引入组件通过以下方式全局注册组件import { createApp } from vue; import { PickerGroup } from vant; const app createApp(); app.use(PickerGroup);PickerGroup在 index.ts 中通过withInstall包装导出同时会声明VanPickerGroup全局组件类型因此既支持app.use()注册也支持按需引入。核心概念统一的标题栏由 PickerGroup 接管使用PickerGroup时有一条必须记住的规则它会代替子组件渲染统一的标题栏。也就是说子选择器不会渲染自己单独的标题栏与标题栏相关的 props 和 events 需要设置到PickerGroup上比如title顶部栏标题confirm事件确认cancel事件取消confirm-button-text、cancel-button-text、show-toolbar等。而子组件中与标题栏无关的属性和事件如v-model、min-date、max-date、columns等可以正常使用。从实现层面看PickerGroup内部复用了 Picker 系列的PickerToolbar组件见 PickerToolbar.tsx通过pickerToolbarProps与pickerToolbarSlots把title、cancelButtonText、confirmButtonText以及toolbar/title/confirm/cancel插槽全部透传给统一的工具栏PickerGroup.tsx。这样所有子选择器共享同一条标题栏视觉上是一个完整连续的交互流程。代码演示选择日期时间在默认插槽中放置一个DatePicker和一个TimePicker即可实现同时选择日期和时间的交互效果van-picker-group title预约日期 :tabs[选择日期, 选择时间] confirmonConfirm cancelonCancel van-date-picker v-modelcurrentDate :min-dateminDate :max-datemaxDate / van-time-picker v-modelcurrentTime / /van-picker-groupimport { ref } from vue; import { showToast } from vant; export default { setup() { const currentDate ref([2022, 06, 01]); const currentTime ref([12, 00]); const onConfirm () { showToast( ${currentDate.value.join(/)} ${currentTime.value.join(:)}, ); }; const onCancel () { showToast(cancel); }; return { minDate: new Date(2020, 0, 1), maxDate: new Date(2025, 5, 1), currentDate, currentTime, onConfirm, onCancel, }; }, };这里tabs数组的每一项对应一个标签页与插槽中子组件的顺序一一对应第一个标签页渲染第一个子组件日期第二个标签页渲染第二个子组件时间。子组件通过v-model各自维护选中值confirm事件触发时PickerGroup会依次调用所有子选择器暴露的confirm()并收集返回值。下一步按钮部分场景下为了保证用户按顺序依次完成所有 Picker 的选择可以设置next-step-text属性。行为规则如下设置了next-step-text后只要用户还未切换到最后一个标签页右上角的按钮就显示为「下一步」点击「下一步」自动切换到下一个 Picker当用户切换到最后一个标签页时右上角按钮变为「确认」。van-picker-group title预约日期 :tabs[选择日期, 选择时间] next-step-text下一步 confirmonConfirm cancelonCancel van-date-picker v-modelcurrentDate :min-dateminDate :max-datemaxDate / van-time-picker v-modelcurrentTime / /van-picker-groupimport { ref } from vue; import { showToast } from vant; export default { setup() { const currentDate ref([2022, 06, 01]); const currentTime ref([12, 00]); const onConfirm () { showToast( ${currentDate.value.join(/)} ${currentTime.value.join(:)}, ); }; const onCancel () { showToast(cancel); }; return { minDate: new Date(2020, 0, 1), maxDate: new Date(2025, 5, 1), currentDate, currentTime, onConfirm, onCancel, }; }, };源码级解读这一步的逻辑在 PickerGroup.tsx 中非常清晰showNextButton()判断当前activeTab是否小于tabs.length - 1且已设置nextStepText点击右上角按钮时若showNextButton()为真则仅把activeTab加 1切换到下一标签页此时不会触发confirm事件只有到达最后一个标签页时才调用children.map((item) item.confirm())收集所有子选择器的值并触发confirm。对应地按钮文字也会动态切换confirmButtonText在显示下一步时被替换为nextStepTextPickerGroup.tsx。测试用例 index.spec.tsx 也验证了这一行为第一次点击时confirm未被触发第二次位于最后一个标签页点击才收集到两个 Picker 的值。选择日期范围在默认插槽中放置两个DatePicker实现选择日期范围的效果。第二个DatePicker的min-date可以基于第一个的选中值通过computed动态计算保证结束日期不早于开始日期van-picker-group title预约日期 :tabs[开始日期, 结束日期] confirmonConfirm cancelonCancel van-date-picker v-modelstartDate :min-dateminDate :max-datemaxDate / van-date-picker v-modelendDate :min-dateendMinDate :max-datemaxDate / /van-picker-groupimport { computed, ref } from vue; import { showToast } from vant; export default { setup() { const startDate ref([2022, 06, 01]); const endDate ref([2023, 06, 01]); const endMinDate computed( () new Date( Number(startDate.value[0]), Number(startDate.value[1]) - 1, Number(startDate.value[2]), ), ); const onConfirm () { showToast(${startDate.value.join(/)} ${endDate.value.join(/)}); }; const onCancel () { showToast(cancel); }; return { minDate: new Date(2020, 0, 1), maxDate: new Date(2025, 5, 1), endMinDate, endDate, startDate, onConfirm, onCancel, }; }, };需要注意日期数组转Date对象时月份要减 1Number(startDate.value[1]) - 1因为 JS 的月份是从 0 开始计数的。选择时间范围与日期范围类似放置两个TimePicker即可实现时间范围选择van-picker-group title预约时间 :tabs[开始时间, 结束时间] confirmonConfirm cancelonCancel van-time-picker v-modelstartTime / van-time-picker v-modelendTime / /van-picker-groupimport { ref } from vue; import { showToast } from vant; export default { setup() { const startTime ref([12, 00]); const endTime ref([12, 00]); const onConfirm () { showToast(${startTime.value.join(:)} ${endTime.value.join(:)}); }; const onCancel () { showToast(cancel); }; return { endTime, startTime, onConfirm, onCancel, }; }, };受控模式PickerGroup中tab的切换支持非受控与受控两种模式非受控未绑定v-model:active-tab时tab 切换完全由组件自身控制受控绑定v-model:active-tab后tab 切换同时受外部绑定值与组件自身操作共同影响。van-button typeprimary clicksetActiveTab 点击切换 tab当前为 {{ activeTab }} /van-button van-picker-group v-model:active-tabactiveTab title预约日期 :tabs[选择日期, 选择时间] confirmonConfirm cancelonCancel van-date-picker v-modelcurrentDate :min-dateminDate :max-datemaxDate / van-time-picker v-modelcurrentTime / /van-picker-groupimport { ref } from vue; import { showToast } from vant; export default { setup() { const activeTab ref(0); const currentDate ref([2022, 06, 01]); const currentTime ref([12, 00]); const setActiveTab () { activeTab.value activeTab.value ? 0 : 1; }; const onConfirm () { showToast( ${currentDate.value.join(/)} ${currentTime.value.join(:)}, ); }; const onCancel () { showToast(cancel); }; return { minDate: new Date(2020, 0, 1), maxDate: new Date(2025, 5, 1), activeTab, currentDate, currentTime, setActiveTab, onConfirm, onCancel, }; }, };源码级解读受控模式的实现基于useSyncPropRef组合式函数use-sync-prop-ref.ts。在 PickerGroup.tsx 中const activeTab useSyncPropRef( () props.activeTab, (value) emit(update:activeTab, value), );它把props.activeTab包装成可同步回写的 ref组件内部切换 tab 时会通过update:activeTab事件回写外部绑定的值同时内部 Tabs 的v-model:active与这个 ref 双向同步。测试用例 index.spec.tsx 验证了外部修改activeTab后对应.van-tab会正确加上van-tab--active类。提示active-tab类型为number | string源码中通过makeNumericProp(0)声明默认值0即默认选中第一个标签页。APIProps参数说明类型默认值v-model:active-tabv4.3.2设置当前选中的标签number | string0tabs设置标签页的标题string[][]title顶部栏标题stringshow-toolbar是否显示顶部栏booleantruenext-step-textv4.0.8下一步按钮的文字stringconfirm-button-text确认按钮的文字string确认cancel-button-text取消按钮的文字string取消补充说明依据源码 PickerGroup.tsx 与 PickerToolbar.tsxtabs通过makeArrayPropstring()声明未传时默认为空数组tabs的长度决定标签页数量建议与插槽中子组件数量保持一致showToolbar通过truthProp声明默认true设为false可隐藏整个顶部栏此时确认/取消按钮也不渲染title、confirmButtonText、cancelButtonText、nextStepText均为可选字符串PickerToolbar中按钮文案有兜底逻辑未显式传入时回退到内置国际化文案t(confirm)/t(cancel)当confirmButtonText与cancelButtonText均为空且未提供对应插槽时对应的按钮不会渲染见 PickerToolbar.tsx。Events事件名说明回调参数confirm点击顶部栏右侧按钮且不在下一步状态时触发所有子选择器confirm()返回值的数组cancel点击顶部栏左侧按钮时触发-update:active-tab内部切换标签页时触发受控模式下同步外部绑定值activeTab的新值其中confirm的参数值得特别注意它是一个数组顺序与子组件一致。每个元素的结构由对应子选择器的confirm()决定例如 Picker 返回的是包含selectedIndexes、selectedOptions、selectedValues的对象参见测试用例 index.spec.tsx。Slots名称说明参数toolbar自定义整个顶部栏的内容-title自定义标题内容-confirm自定义确认按钮内容-cancel自定义取消按钮内容-这些插槽与 Picker 的pickerToolbarSlots一一对应PickerToolbar.tsx。当toolbar插槽存在时它会完全替换默认的取消 标题 确认布局PickerToolbar.tsx。类型定义组件导出以下类型定义import type { PickerGroupProps, PickerGroupThemeVars } from vant;PickerGroupPropsExtractPropTypestypeof pickerGroupProps即所有 props 的联合类型PickerGroup.tsxPickerGroupThemeVars主题变量类型当前包含可选的pickerGroupBackgroundtypes.ts。主题定制样式变量组件提供下列 CSS 变量用于自定义样式使用方法请参考 ConfigProvider 组件。名称默认值描述--van-picker-group-background--van-background-2选择器组背景色从样式源码 index.less 可以看到--van-picker-group-background在:root/:host中默认引用--van-background-2作用于.van-picker-group的背景.van-picker-group__tabs顶部留有--van-padding-base的间距.van-picker-group__tab-title标签标题右侧有16px间距用于分隔多个标签页标题。在业务中可通过ConfigProvider的theme-vars或直接覆盖 CSS 变量来调整van-config-provider :theme-vars{ pickerGroupBackground: #f7f8fa } van-picker-group ... !-- 子选择器 -- /van-picker-group /van-config-provider实现原理与测试验证标签页渲染机制PickerGroup的内部结构PickerGroup.tsx为div classvan-picker-group Toolbar ... / !-- 统一标题栏可隐藏 -- Tabs classvan-picker-group__tabs shrink animated lazyRender{false} Tab v-fortab in tabs childNodes[index] /Tab /Tabs /div几个值得注意的实现细节内部 Tabs 设置了shrink标签页宽度收缩自适应与animated滑动切换动画且lazyRender{false}即所有标签页会一次性渲染默认插槽的节点会先过滤掉注释节点、展开Fragment并对数组做扁平化处理PickerGroup.tsx因此支持用v-for渲染子选择器——测试用例 index.spec.tsx 专门验证了该场景每个.van-swipe-item内都能找到对应的.van-picker。父子通信为什么子组件无需额外配置PickerGroup与子选择器之间通过 Vue 的provide/inject机制通信PickerGroup用useChildren(PICKER_GROUP_KEY)注册为父组件并持有所有子选择器的引用PickerGroup.tsxPicker 内部用useParent(PICKER_GROUP_KEY)注册为子组件Picker.tsx点击确认时PickerGroup遍历children依次调用每个子选择器的confirm()方法收集结果。这也是其他基于 Picker 封装的自定义组件可以直接放入PickerGroup的原因只要封装组件内部保留useParent(PICKER_GROUP_KEY)调用并暴露confirm()方法就能自动接入统一标题栏、下一步分步与结果收集机制。完整可用 Demo仓库中提供了完整的可运行示例位于 packages/vant/src/picker-group/demo 目录下与本文各小节一一对应SelectDateTime.vue选择日期时间NextStepButton.vue下一步按钮SelectDateRange.vue选择日期范围SelectTimeRange.vue选择时间范围ControlTab.vue受控模式index.vueDemo 汇总入口。总结PickerGroup通过统一标题栏 Tabs 分步 子组件结果收集三位一体的设计把多个 Picker 类组件的多次交互折叠为一次流畅的流程是预约、筛选、日期/时间范围等场景的推荐方案。使用时记住三条核心规则标题栏相关 props 与 events 一律设置在PickerGroup上子组件只关心自己的取值逻辑tabs数组与子组件顺序一一对应需要分步引导时配置next-step-text未走完所有标签页前不会触发confirm需要外部控制标签页时绑定v-model:active-tab进入受控模式配合confirm回调的数组参数统一收集所有选择结果。深入阅读源码可进一步掌握其实现PickerGroup.tsx核心实现、PickerToolbar.tsx统一工具栏、index.spec.tsx行为测试、index.less样式变量。【免费下载链接】vantA lightweight, customizable Vue UI library for mobile web apps.项目地址: https://gitcode.com/GitHub_Trending/va/vant创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表