PickerGroup
PickerGroup是基于 Vant 官方PickerGroup做的 Formily 字段封装,交互层面和Picker一样通过点击字段打开弹层,但弹层内部会按 tab 分步完成多项选择。
与当前封装的约定
- 当前封装固定通过
Popup弹层承载,不需要手动维护show Field上的dataSource仍然使用对象数组,每一项对应一个 tab- 不提供默认插槽时,会回退到内部
VanPicker,此时每个 tab 只支持单列选项,最终字段值会写回成扁平数组 - 提供默认插槽时,会优先渲染插槽里的
Picker、Area、DatePicker、TimePicker或基于Picker封装的自定义组件 tabs、activeTab和弹层show依然是内部状态,但默认插槽会额外暴露activeTab/setActiveTab/values/setValue供高级场景使用- 中间步骤的确认按钮会自动切到下一步,最后一步才真正写回字段值
基础使用
vue
<script setup lang="ts">
import { createForm } from '@formily/core'
import { Form, FormButtonGroup, FormItem, PickerGroup, Submit } from '@silver-formily/vant'
import { Field } from '@silver-formily/vue'
import { showDemoResult } from '../shared'
import { appointmentOptions } from './shared'
const form = createForm({
values: {
appointment: ['sh', 'night'],
},
})
async function handleSubmit(values: typeof form.values) {
await showDemoResult(values)
}
</script>
<template>
<Form :form="form">
<Field
name="appointment"
title="预约信息"
:decorator="[FormItem, { isLink: true }]"
:component="[PickerGroup]"
:data-source="appointmentOptions"
/>
<FormButtonGroup>
<Submit :on-submit="handleSubmit">
查看结果
</Submit>
</FormButtonGroup>
</Form>
</template>自定义步骤内容
vue
<script setup lang="ts">
import type { PickerGroupResolvedValue } from '@silver-formily/vant'
import { createForm } from '@formily/core'
import { DatePicker, Form, FormButtonGroup, FormItem, PickerGroup, Submit } from '@silver-formily/vant'
import { Field } from '@silver-formily/vue'
import { TimePicker } from 'vant'
import { showDemoResult } from '../shared'
import { scheduleTabs } from './shared'
const form = createForm({
values: {
schedule: ['2026-03-30', ['09', '30']],
},
})
const minDate = new Date(2025, 0, 1)
const maxDate = new Date(2027, 11, 31)
function formatSchedule(value: PickerGroupResolvedValue) {
const [date = '', time = []] = value ?? []
const dateText = String(date ?? '')
const timeText = Array.isArray(time)
? time.join(':')
: String(time ?? '')
return [dateText, timeText]
.filter(Boolean)
.join(' / ')
}
async function handleSubmit(values: typeof form.values) {
await showDemoResult(values)
}
</script>
<template>
<Form :form="form">
<Field
name="schedule"
title="预约时间"
:decorator="[FormItem, { isLink: true }]"
:component="[PickerGroup, { displayFormatter: formatSchedule }]"
:data-source="scheduleTabs"
>
<DatePicker :min-date="minDate" :max-date="maxDate" />
<TimePicker />
</Field>
<FormButtonGroup>
<Submit :on-submit="handleSubmit">
查看结果
</Submit>
</FormButtonGroup>
</Form>
</template>Area 与 TimePicker 组合
vue
<script setup lang="ts">
import type { PickerGroupResolvedValue } from '@silver-formily/vant'
import { createForm } from '@formily/core'
import { Form, FormButtonGroup, FormItem, PickerGroup, Submit } from '@silver-formily/vant'
import { Field } from '@silver-formily/vue'
import { areaList } from '@vant/area-data'
import { Area, TimePicker } from 'vant'
import { showDemoResult } from '../shared'
import { deliveryTabs } from './shared'
const form = createForm({
values: {
delivery: ['330106', ['19', '30']],
},
})
function resolveAreaText(code: string) {
if (!code)
return ''
const province = areaList.province_list[`${code.slice(0, 2)}0000`]
const city = areaList.city_list[`${code.slice(0, 4)}00`]
const county = areaList.county_list[code]
return [province, city, county]
.filter(Boolean)
.join(' / ')
}
function formatDelivery(value: PickerGroupResolvedValue) {
const [areaCode = '', time = []] = value ?? []
const areaText = typeof areaCode === 'string'
? resolveAreaText(areaCode)
: ''
const timeText = Array.isArray(time)
? time.join(':')
: String(time ?? '')
return [areaText, timeText]
.filter(Boolean)
.join(' | ')
}
async function handleSubmit(values: typeof form.values) {
await showDemoResult(values)
}
</script>
<template>
<Form :form="form">
<Field
name="delivery"
title="配送时间"
:decorator="[FormItem, { isLink: true }]"
:component="[PickerGroup, { displayFormatter: formatDelivery }]"
:data-source="deliveryTabs"
>
<Area :area-list="areaList" />
<TimePicker />
</Field>
<FormButtonGroup>
<Submit :on-submit="handleSubmit">
查看结果
</Submit>
</FormButtonGroup>
</Form>
</template>工具栏与选项插槽
vue
<script setup lang="ts">
import { createForm } from '@formily/core'
import { FormItem, PickerGroup } from '@silver-formily/vant'
import { Field, FormProvider } from '@silver-formily/vue'
import { Tag } from 'vant'
import { appointmentOptions } from './shared'
const form = createForm({
values: {
appointment: ['hz', 'am'],
},
})
</script>
<template>
<FormProvider :form="form">
<div class="demo-panel">
<Field
name="appointment"
title="自定义插槽"
:decorator="[FormItem, { isLink: true }]"
:component="[PickerGroup]"
:data-source="appointmentOptions"
>
<template #title>
<div class="picker-group-slot-title">
分步选择预约
</div>
</template>
<template #cancel>
<span class="picker-group-slot-action">关闭</span>
</template>
<template #confirm>
<span class="picker-group-slot-action picker-group-slot-action--confirm">下一步 / 完成</span>
</template>
<template #option="option">
<div class="picker-group-slot-option">
<span>{{ option.text }}</span>
<Tag plain size="medium" :type="['sh', 'night'].includes(option.value) ? 'danger' : 'primary'">
{{ ['sh', 'night'].includes(option.value) ? '热门' : '常用' }}
</Tag>
</div>
</template>
</Field>
</div>
</FormProvider>
</template>
<style scoped>
.picker-group-slot-title {
padding: 8px 0;
font-size: 16px;
font-weight: 600;
}
.picker-group-slot-action {
font-size: 14px;
color: var(--van-text-color-2);
}
.picker-group-slot-action--confirm {
color: var(--van-primary-color);
}
.picker-group-slot-option {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
width: 100%;
}
</style>API
使用约定
dataSource的推荐结构如下:
ts
interface PickerGroupDataSourceItem {
title: string
options?: PickerColumn
}
type PickerGroupDataSource = PickerGroupDataSourceItem[]title用于生成内部 tab 标题- 不提供默认插槽时,
options作为对应步骤的单列选项 - 提供默认插槽时,
options可以省略,dataSource仅用于提供 tab 标题 - 默认模式下最终字段值类型为
Array<string | number> | null - 默认插槽模式下,最终字段值会按 tab 保留每个子组件自己的
modelValue结构,例如['2026-03-30', ['09', '30']] - 触发区默认把每个 tab 的选中文案用
separator拼接,默认值为' / ' readPretty模式下会自动回显当前选项文案;如果使用默认插槽模式,推荐同时提供displayFormatter- 当前仍然不提供外部
activeTabprop /v-model:active-tab控制模型
封装补充 Props
| 属性名 | 类型 | 描述 | 默认值 |
|---|---|---|---|
modelValue | Array<string | number | Array<string | number>> | null | 当前字段值 | - |
dataSource | PickerGroupDataSource | 分步数据;默认模式下提供 tab + 选项,默认插槽模式下只需要提供 tab 标题 | [] |
columnsFieldNames | object | 自定义选项字段名映射,仅默认模式生效 | 官方默认值 |
placeholder | string | 未选择时的展示文案 | '请选择选项' |
separator | string | 字段展示区分隔符 | ' / ' |
displayFormatter | Function | 自定义字段展示区文案;默认插槽模式建议显式提供 | - |
readonly | boolean | 只读态,阻止打开弹层 | false |
disabled | boolean | 禁用态,阻止打开弹层 | false |
内部 Picker Props
以下属性会统一透传到每个步骤内部的 Picker;如果使用默认插槽模式,也会在未显式声明同名 prop 时注入给子组件:
| 属性名 | 类型 | 描述 | 默认值 |
|---|---|---|---|
title | string | 顶部标题 | 官方默认值 |
cancelButtonText | string | 取消按钮文案 | 官方默认值 |
confirmButtonText | string | 最后一步确认文案 | 官方默认值 |
optionHeight | number | string | 选项高度 | 官方默认值 |
visibleOptionNum | number | string | 可见选项个数 | 官方默认值 |
swipeDuration | number | string | 滚动惯性动画时长 | 官方默认值 |
allowHtml | boolean | 是否渲染 HTML 文案 | 官方默认值 |
内部 Popup Props
当前封装内部固定包了一层 Popup,以下弹层属性可直接使用:
| 属性名 | 类型 | 描述 | 默认值 |
|---|---|---|---|
position | enum | 弹出位置 | 'bottom' |
round | boolean | 是否显示圆角 | true |
overlay | boolean | 是否显示遮罩层 | true |
teleport | string | Element | 指定挂载节点 | 官方默认值 |
closeOnPopstate | boolean | 回退时是否自动关闭 | true |
closeOnClickOverlay | boolean | 点击遮罩是否自动关闭 | true |
safeAreaInsetTop | boolean | 是否开启顶部安全区 | 官方默认值 |
safeAreaInsetBottom | boolean | 是否开启底部安全区 | true |
lockScroll | boolean | 是否锁定背景滚动 | true |
lazyRender | boolean | 是否延迟渲染内容 | true |
zIndex | number | string | 弹层层级 | 官方默认值 |
duration | number | string | 动画时长 | 官方默认值 |
transition | string | 自定义过渡动画 | 官方默认值 |
Slots
以下插槽已转发:
| 插槽名 | 描述 | 插槽参数 |
|---|---|---|
default | 自定义步骤内容,可直接放 Picker、Area、DatePicker、TimePicker 等子组件 | { activeTab, modelValue, values, setValue, setActiveTab } |
title | 自定义顶部标题 | - |
cancel | 自定义取消按钮 | - |
confirm | 自定义确认按钮 | - |
toolbar | 自定义整个工具栏 | - |
option | 自定义默认模式下每个步骤里 Picker 的选项内容 | option |
empty | 自定义默认模式下的空状态内容 | - |
Events
| 事件名 | 描述 | 回调参数 |
|---|---|---|
update:modelValue | 最后一步确认后同步字段值 | Function |
change | 任一步骤的选项切换时触发 | Function |
confirm | 最后一步点击确认时触发 | Function |
cancel | 点击取消按钮时触发 | Function |
clickOption | 点击某个选项时触发 | Function |
scrollInto | 滚动到某个选项时触发 | Function |
open | 弹层打开时触发 | - |
close | 弹层关闭时触发 | - |
opened | 弹层打开且动画结束后触发 | - |
closed | 弹层关闭且动画结束后触发 | - |
clickOverlay | 点击遮罩层时触发 | Function |
update:show | 弹层开关变化时触发 | Function |