Cascader
Cascader是基于 Vant 官方Cascader做的表单封装,补上了移动端表单里常见的“字段触发区 + 内置弹层 + 阅读态路径展示”这层能力。
与官方的差异
- 当前封装固定使用
Popup弹层,不需要手动维护show Field上的dataSource会自动映射到options- 字段值默认保存为完整路径数组,例如
['zj', 'hz', 'xh'],而不是 Vant 原始的叶子值 - 未完成的中间选择在关闭弹层后会自动回滚,只有选到叶子节点时才会更新字段值
基础使用
vue
<script setup lang="ts">
import { createForm } from '@formily/core'
import { Cascader, Form, FormButtonGroup, FormItem, Submit } from '@silver-formily/vant'
import { Field } from '@silver-formily/vue'
import { showDemoResult } from '../shared'
import { cityOptions } from './shared'
const form = createForm({
values: {
area: ['330000', '330100', '330106'],
},
})
async function handleSubmit(values: typeof form.values) {
await showDemoResult(values)
}
</script>
<template>
<Form :form="form">
<Field
name="area"
title="所在地区"
:decorator="[FormItem, { isLink: true }]"
:component="[Cascader]"
:data-source="cityOptions"
/>
<FormButtonGroup>
<Submit :on-submit="handleSubmit">
查看结果
</Submit>
</FormButtonGroup>
</Form>
</template>中国省市区数据
文档站已经安装 @vant/area-data,这里直接使用官方推荐的 useCascaderAreaData() 来生成选项数据。
vue
<script setup lang="ts">
import { createForm } from '@formily/core'
import { Cascader, Form, FormButtonGroup, FormItem, Submit } from '@silver-formily/vant'
import { Field } from '@silver-formily/vue'
import { useCascaderAreaData } from '@vant/area-data'
import { showDemoResult } from '../shared'
const form = createForm({
values: {
areaCode: null,
},
})
const areaOptions = useCascaderAreaData()
async function handleSubmit(values: typeof form.values) {
await showDemoResult(values)
}
</script>
<template>
<Form :form="form">
<Field
name="areaCode"
title="省市区"
:decorator="[FormItem, { isLink: true }]"
:component="[Cascader]"
:data-source="areaOptions"
/>
<FormButtonGroup>
<Submit :on-submit="handleSubmit">
查看结果
</Submit>
</FormButtonGroup>
</Form>
</template>自定义颜色
vue
<script setup lang="ts">
import { createForm } from '@formily/core'
import { Cascader, FormItem } from '@silver-formily/vant'
import { Field, FormProvider } from '@silver-formily/vue'
import { cityOptions } from './shared'
const form = createForm({
values: {
area: ['320000', '320100', '320106'],
},
})
</script>
<template>
<FormProvider :form="form">
<div class="demo-panel">
<Field
name="area"
title="自定义颜色"
:decorator="[FormItem, { isLink: true }]"
:component="[
Cascader,
{
activeColor: '#ee0a24',
},
]"
:data-source="cityOptions"
/>
</div>
</FormProvider>
</template>异步加载选项
change 事件的 payload 会额外携带当前字段实例 field,因此可以直接在回调里通过 field.setDataSource(...) 更新当前字段的数据源,不需要再额外维护一份本地 options 状态。
vue
<script setup lang="ts">
import type { DataField } from '@formily/core'
import type { CascaderChangeEvent, CascaderOption } from '@silver-formily/vant'
import { createForm, onFieldInit } from '@formily/core'
import { Cascader, FormItem } from '@silver-formily/vant'
import { Field, FormProvider } from '@silver-formily/vue'
import { closeToast, showLoadingToast } from 'vant'
const asyncChildrenMap: Record<string, CascaderOption[]> = {
330000: [
{ text: '杭州市', value: '330100' },
{ text: '宁波市', value: '330200' },
],
}
function createInitialOptions(): CascaderOption[] {
return [
{
text: '浙江省',
value: '330000',
children: [],
},
]
}
function resolveAsyncChildren(value: string | number): CascaderOption[] {
return (asyncChildrenMap[String(value)] ?? []).map(option => ({ ...option }))
}
const form = createForm({
effects() {
onFieldInit('asyncArea', (field: DataField) => {
field.setDataSource(createInitialOptions())
})
},
})
function onChange({ value, field }: CascaderChangeEvent) {
const currentOptions = Array.isArray(field?.dataSource)
? field.dataSource as CascaderOption[]
: []
const currentOption = currentOptions[0]
if (!currentOption || value !== currentOption.value || currentOption.children?.length) {
return
}
showLoadingToast({
message: '加载中...',
duration: 0,
forbidClick: true,
})
window.setTimeout(() => {
const nextOptions = currentOptions.map((option) => {
if (option.value !== value) {
return option
}
return {
...option,
children: resolveAsyncChildren(value),
}
})
field?.setDataSource(nextOptions)
closeToast()
}, 1000)
}
</script>
<template>
<FormProvider :form="form">
<div class="demo-panel">
<Field
name="asyncArea"
title="异步加载选项"
:decorator="[FormItem, { isLink: true }]"
:component="[
Cascader,
{
onChange,
},
]"
/>
</div>
</FormProvider>
</template>自定义字段名
vue
<script setup lang="ts">
import { createForm } from '@formily/core'
import { Cascader, FormItem } from '@silver-formily/vant'
import { Field, FormProvider } from '@silver-formily/vue'
import { customFieldOptions, fieldNames } from './shared'
const form = createForm({
values: {
areaCode: ['330000', '330100'],
},
})
</script>
<template>
<FormProvider :form="form">
<div class="demo-panel">
<Field
name="areaCode"
title="自定义字段名"
:decorator="[FormItem, { isLink: true }]"
:component="[
Cascader,
{
fieldNames,
},
]"
:data-source="customFieldOptions"
/>
</div>
</FormProvider>
</template>自定义选项上方内容
vue
<script setup lang="ts">
import { createForm } from '@formily/core'
import { Cascader, FormItem } from '@silver-formily/vant'
import { Field, FormProvider } from '@silver-formily/vue'
import { cityOptions } from './shared'
const form = createForm({
values: {
area: null,
},
})
</script>
<template>
<FormProvider :form="form">
<div class="demo-panel">
<Field
name="area"
title="自定义选项上方内容"
:decorator="[FormItem, { isLink: true }]"
:component="[Cascader]"
:data-source="cityOptions"
>
<template #options-top="{ tabIndex }">
<div class="cascader-level-tip">
当前为第 {{ tabIndex + 1 }} 级
</div>
</template>
</Field>
</div>
</FormProvider>
</template>
<style scoped>
.cascader-level-tip {
padding: 12px 16px 8px;
font-size: 12px;
color: var(--van-text-color-2);
background: var(--van-gray-1);
}
</style>补充:自定义标题与选项
vue
<script setup lang="ts">
import { createForm } from '@formily/core'
import { Cascader, Form, FormItem } from '@silver-formily/vant'
import { Field } from '@silver-formily/vue'
import { Tag } from 'vant'
import { cityOptions } from './shared'
const form = createForm({
values: {
serviceRegion: ['320000', '320100', '320106'],
},
})
</script>
<template>
<Form :form="form">
<Field
name="serviceRegion"
title="服务区域"
:decorator="[FormItem, { isLink: true }]"
:component="[
Cascader,
{
activeColor: '#ee0a24',
closeIcon: 'cross',
},
]"
:data-source="cityOptions"
>
<template #title>
<div class="demo-cascader-title">
选择服务片区
</div>
</template>
<template #option="{ option }">
<div class="demo-cascader-option">
<span>{{ option.text }}</span>
<Tag
plain
size="medium"
:type="option.children?.length ? 'primary' : 'success'"
>
{{ option.children?.length ? '继续选择' : '最终节点' }}
</Tag>
</div>
</template>
</Field>
</Form>
</template>
<style scoped>
.demo-cascader-title {
padding: 8px 0;
font-size: 16px;
font-weight: 600;
}
.demo-cascader-option {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
width: 100%;
}
</style>API
使用约定
readPretty模式下会自动展示完整路径文案readonly/readOnly/disabled都会阻止弹层打开change事件仍然保留逐级选择时的中间态;真正写回字段的是叶子节点触发后的update:modelValuechange/finish的 payload 会额外暴露当前field- 异步联动场景里更推荐在
change回调中直接使用payload.field.setDataSource(...)更新字段数据源
封装补充 Props
| 属性名 | 类型 | 描述 | 默认值 |
|---|---|---|---|
modelValue | string[] | number[] | string | number | - | 当前值,兼容叶子值输入 | - |
separator | string | 字段展示区路径分隔符 | ' / ' |
displayFormatter | Function | 自定义展示区文案 | - |
readonly | boolean | 只读态,阻止打开弹层 | false |
disabled | boolean | 禁用态,阻止打开弹层 | false |
官方 Cascader Props
以下属性会直接透传给 Vant Cascader:
| 属性名 | 类型 | 描述 | 默认值 |
|---|---|---|---|
title | string | 标题 | 官方默认值 |
options | CascaderOption[] | 选项列表 | [] |
fieldNames | object | 自定义字段名映射 | 官方默认值 |
placeholder | string | 未选中时的文案 | 官方默认值 |
activeColor | string | 激活态颜色 | 官方默认值 |
swipeable | boolean | 是否支持手势切换 | 官方默认值 |
showHeader | boolean | 是否展示头部 | 官方默认值 |
closeable | boolean | 是否显示关闭图标 | 官方默认值 |
closeIcon | string | 关闭图标名称 | 官方默认值 |
官方 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
以下官方插槽已转发:
| 插槽名 | 描述 | 插槽参数 |
|---|---|---|
title | 自定义标题 | - |
option | 自定义选项内容 | object |
options-top | 选项区顶部 | object |
options-bottom | 选项区底部 | object |
Events
| 事件名 | 描述 | 回调参数 |
|---|---|---|
update:modelValue | 选中叶子节点后同步字段值 | Function |
change | 任意层级切换时触发 | Function,其中 payload.field 为当前字段实例 |
finish | 选中叶子节点时触发 | Function |
open | 弹层打开时触发 | - |
close | 弹层关闭时触发 | - |
opened | 弹层打开且动画结束后触发 | - |
closed | 弹层关闭且动画结束后触发 | - |
clickTab | 点击任意级别的标签页时触发 | Function |
clickOverlay | 点击遮罩层时触发 | Function |
update:show | 弹层开关变化时触发 | Function |