FormStep
FormStep用来把一组 Schema 字段拆成移动端更常见的分步提交流程,外部既可以通过FormStep.createFormStep()控制前进、后退和最终提交,也可以在FormStep子树中通过useFormStep()读取组件内部自动创建的实例。
使用限制
当前封装和 element-plus 版本保持一致,只适用于 Schema 场景,推荐和 SchemaField、SchemaVoidField 一起使用。
渲染更新
createFormStep() 返回的是 @formily/reactive 响应式模型。示例里使用 FormConsumer 搭配 FormButtonGroup 包裹按钮区,让“上一步 / 下一步 / 提交”的禁用状态跟着步骤自动刷新,同时也能沿用统一的按钮布局配置。
Markup Schema
vue
<script setup lang="ts">
import { createForm } from '@formily/core'
import { Form, FormButtonGroup, FormItem, FormStep, Input, Submit } from '@silver-formily/vant'
import { createSchemaField, FormConsumer } from '@silver-formily/vue'
import { Button as VanButton } from 'vant'
import { showDemoResult } from '../shared'
const { SchemaField, SchemaStringField, SchemaVoidField } = createSchemaField({
components: {
FormItem,
FormStep,
Input,
},
})
const form = createForm()
const formStep = FormStep.createFormStep()
async function handleSubmit(values: typeof form.values) {
await showDemoResult(values, 'Markup Schema 提交结果')
}
</script>
<template>
<Form :form="form">
<SchemaField>
<SchemaVoidField x-component="FormStep" :x-component-props="{ formStep }">
<SchemaVoidField
x-component="FormStep.StepPane"
:x-component-props="{
title: '基础信息',
}"
>
<SchemaStringField
name="name"
title="姓名"
required
x-decorator="FormItem"
x-component="Input"
:x-component-props="{ placeholder: '请输入姓名' }"
/>
<SchemaStringField
name="mobile"
title="手机号"
required
x-decorator="FormItem"
x-component="Input"
:x-component-props="{ placeholder: '请输入手机号' }"
/>
</SchemaVoidField>
<SchemaVoidField
x-component="FormStep.StepPane"
:x-component-props="{
title: '配送信息',
}"
>
<SchemaStringField
name="address"
title="收货地址"
required
x-decorator="FormItem"
x-component="Input.TextArea"
:x-component-props="{ rows: 3, placeholder: '请输入详细地址' }"
/>
<SchemaStringField
name="remark"
title="补充说明"
x-decorator="FormItem"
x-component="Input.TextArea"
:x-component-props="{ rows: 2, placeholder: '如门牌号、楼层、到访时间等' }"
/>
</SchemaVoidField>
<SchemaVoidField
x-component="FormStep.StepPane"
:x-component-props="{
title: '确认提交',
}"
>
<SchemaStringField
name="summary"
title="备注摘要"
x-decorator="FormItem"
x-component="Input.TextArea"
:x-component-props="{ rows: 2, placeholder: '选填,用于演示最后一步也能继续编辑字段' }"
/>
</SchemaVoidField>
</SchemaVoidField>
</SchemaField>
<FormConsumer>
<template #default>
<FormButtonGroup layout="horizontal">
<VanButton
:disabled="!formStep.allowBack"
plain
type="default"
@click="formStep.back()"
>
上一步
</VanButton>
<VanButton
v-if="formStep.allowNext"
type="primary"
@click="formStep.next()"
>
下一步
</VanButton>
<Submit v-else :on-submit="handleSubmit">
提交
</Submit>
</FormButtonGroup>
</template>
</FormConsumer>
</Form>
</template>JSON Schema
vue
<script setup lang="ts">
import { createForm } from '@formily/core'
import { Form, FormButtonGroup, FormItem, FormStep, Input, Submit } from '@silver-formily/vant'
import { createSchemaField, FormConsumer } from '@silver-formily/vue'
import { Button as VanButton } from 'vant'
import { showDemoResult } from '../shared'
const { SchemaField } = createSchemaField({
components: {
FormItem,
FormStep,
Input,
},
})
const form = createForm()
const formStep = FormStep.createFormStep()
const schema = {
type: 'object',
properties: {
stepper: {
'type': 'void',
'x-component': 'FormStep',
'x-component-props': {
formStep: '{{formStep}}',
activeColor: '#1989fa',
},
'properties': {
basic: {
'type': 'void',
'x-component': 'FormStep.StepPane',
'x-component-props': {
title: '账号信息',
},
'properties': {
username: {
'type': 'string',
'title': '用户名',
'required': true,
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
placeholder: '请输入用户名',
},
},
password: {
'type': 'string',
'title': '密码',
'required': true,
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
placeholder: '请输入密码',
type: 'password',
},
},
},
},
profile: {
'type': 'void',
'x-component': 'FormStep.StepPane',
'x-component-props': {
title: '资料补充',
},
'properties': {
nickname: {
'type': 'string',
'title': '昵称',
'required': true,
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
placeholder: '请输入昵称',
},
},
intro: {
'type': 'string',
'title': '简介',
'x-decorator': 'FormItem',
'x-component': 'Input.TextArea',
'x-component-props': {
rows: 2,
placeholder: '介绍一下自己',
},
},
},
},
},
},
},
}
async function handleSubmit(values: typeof form.values) {
await showDemoResult(values, 'JSON Schema 提交结果')
}
</script>
<template>
<Form :form="form">
<SchemaField :schema="schema" :scope="{ formStep }" />
<FormConsumer>
<template #default>
<FormButtonGroup layout="horizontal">
<VanButton
:disabled="!formStep.allowBack"
plain
type="default"
@click="formStep.back()"
>
上一步
</VanButton>
<VanButton
v-if="formStep.allowNext"
type="primary"
@click="formStep.next()"
>
下一步
</VanButton>
<Submit v-else :on-submit="handleSubmit">
提交
</Submit>
</FormButtonGroup>
</template>
</FormConsumer>
</Form>
</template>内部创建实例
vue
<script setup lang="tsx">
import type { Form as IForm } from '@formily/core'
import { createForm } from '@formily/core'
import { Form, FormButtonGroup, FormItem, FormStep, Input, Submit, useFormStep } from '@silver-formily/vant'
import { createSchemaField } from '@silver-formily/vue'
import { Button as VanButton } from 'vant'
import { defineComponent } from 'vue'
import { showDemoResult } from '../shared'
const InternalActions = defineComponent({
name: 'InternalActions',
props: {
resultTitle: {
type: String,
default: '内部创建 FormStep 提交结果',
},
},
setup(props) {
const formStep = useFormStep()
async function handleSubmit(values: IForm['values']) {
await showDemoResult(values, props.resultTitle)
}
return () => (
<FormButtonGroup layout="horizontal">
<VanButton
disabled={!formStep.value.allowBack}
plain
type="default"
onClick={() => formStep.value.back()}
>
上一步
</VanButton>
{formStep.value.allowNext
? (
<VanButton
type="primary"
onClick={() => formStep.value.next()}
>
下一步
</VanButton>
)
: (
<Submit onSubmit={handleSubmit}>
提交
</Submit>
)}
</FormButtonGroup>
)
},
})
const { SchemaField, SchemaStringField, SchemaVoidField } = createSchemaField({
components: {
FormItem,
FormStep,
Input,
InternalActions,
},
})
const form = createForm()
</script>
<template>
<Form :form="form">
<div class="internal-tip">
这里没有手动调用 <code>FormStep.createFormStep()</code>,底部按钮通过 <code>useFormStep()</code> 直接读取组件内部创建的步骤实例。
</div>
<SchemaField>
<SchemaVoidField x-component="FormStep" :x-component-props="{ activeColor: '#1989fa' }">
<SchemaVoidField
x-component="FormStep.StepPane"
:x-component-props="{
title: '联系信息',
}"
>
<SchemaStringField
name="name"
title="姓名"
required
x-decorator="FormItem"
x-component="Input"
:x-component-props="{ placeholder: '请输入姓名' }"
/>
<SchemaStringField
name="mobile"
title="手机号"
required
x-decorator="FormItem"
x-component="Input"
:x-component-props="{ placeholder: '请输入手机号' }"
/>
<SchemaVoidField x-component="InternalActions" />
</SchemaVoidField>
<SchemaVoidField
x-component="FormStep.StepPane"
:x-component-props="{
title: '配送信息',
}"
>
<SchemaStringField
name="city"
title="城市"
required
x-decorator="FormItem"
x-component="Input"
:x-component-props="{ placeholder: '请输入城市' }"
/>
<SchemaStringField
name="address"
title="详细地址"
required
x-decorator="FormItem"
x-component="Input.TextArea"
:x-component-props="{ rows: 2, placeholder: '请输入街道、楼栋和门牌号' }"
/>
<SchemaVoidField x-component="InternalActions" />
</SchemaVoidField>
<SchemaVoidField
x-component="FormStep.StepPane"
:x-component-props="{
title: '确认提交',
}"
>
<SchemaStringField
name="remark"
title="备注"
x-decorator="FormItem"
x-component="Input.TextArea"
:x-component-props="{ rows: 2, placeholder: '选填,补充配送时间或上门说明' }"
/>
<SchemaVoidField
x-component="InternalActions"
:x-component-props="{ resultTitle: '内部自动创建步骤实例提交结果' }"
/>
</SchemaVoidField>
</SchemaVoidField>
</SchemaField>
</Form>
</template>
<style scoped>
.internal-tip {
margin: 0 12px 12px;
padding: 10px 12px;
border-radius: 12px;
font-size: 12px;
line-height: 1.6;
color: #1989fa;
background: rgba(25, 137, 250, 0.08);
}
.internal-tip code {
padding: 0 4px;
border-radius: 4px;
color: inherit;
background: rgba(25, 137, 250, 0.12);
}
</style>隐藏步骤条
vue
<script setup lang="ts">
import { createForm } from '@formily/core'
import { Form, FormButtonGroup, FormItem, FormStep, Input, Submit } from '@silver-formily/vant'
import { createSchemaField, FormConsumer } from '@silver-formily/vue'
import { Button as VanButton } from 'vant'
import { showDemoResult } from '../shared'
const { SchemaField, SchemaStringField, SchemaVoidField } = createSchemaField({
components: {
FormItem,
FormStep,
Input,
},
})
const form = createForm()
const formStep = FormStep.createFormStep()
async function handleSubmit(values: typeof form.values) {
await showDemoResult(values, '隐藏步骤条提交结果')
}
</script>
<template>
<Form :form="form">
<div class="hide-steps-tip">
顶部步骤条已隐藏,适合把分步流程嵌进弹窗、抽屉或卡片内容区。
</div>
<SchemaField>
<SchemaVoidField x-component="FormStep" :x-component-props="{ formStep, hideSteps: true }">
<SchemaVoidField
x-component="FormStep.StepPane"
:x-component-props="{
title: '联系信息',
}"
>
<SchemaStringField
name="name"
title="姓名"
required
x-decorator="FormItem"
x-component="Input"
:x-component-props="{ placeholder: '请输入姓名' }"
/>
<SchemaStringField
name="mobile"
title="手机号"
required
x-decorator="FormItem"
x-component="Input"
:x-component-props="{ placeholder: '请输入手机号' }"
/>
</SchemaVoidField>
<SchemaVoidField
x-component="FormStep.StepPane"
:x-component-props="{
title: '配送信息',
}"
>
<SchemaStringField
name="address"
title="收货地址"
required
x-decorator="FormItem"
x-component="Input.TextArea"
:x-component-props="{ rows: 3, placeholder: '请输入详细地址' }"
/>
</SchemaVoidField>
<SchemaVoidField
x-component="FormStep.StepPane"
:x-component-props="{
title: '确认提交',
}"
>
<SchemaStringField
name="remark"
title="备注"
x-decorator="FormItem"
x-component="Input.TextArea"
:x-component-props="{ rows: 2, placeholder: '可选,补充配送时间或到访说明' }"
/>
</SchemaVoidField>
</SchemaVoidField>
</SchemaField>
<FormConsumer>
<template #default>
<FormButtonGroup layout="horizontal">
<VanButton
:disabled="!formStep.allowBack"
plain
type="default"
@click="formStep.back()"
>
上一步
</VanButton>
<VanButton
v-if="formStep.allowNext"
type="primary"
@click="formStep.next()"
>
下一步
</VanButton>
<Submit v-else :on-submit="handleSubmit">
提交
</Submit>
</FormButtonGroup>
</template>
</FormConsumer>
</Form>
</template>
<style scoped>
.hide-steps-tip {
margin: 0 12px 12px;
padding: 10px 12px;
border-radius: 12px;
font-size: 12px;
line-height: 1.6;
color: #1989fa;
background: rgba(25, 137, 250, 0.08);
}
</style>插槽自定义渲染
vue
<script setup lang="tsx">
import { createForm } from '@formily/core'
import { Form, FormButtonGroup, FormItem, FormStep, Input, Submit } from '@silver-formily/vant'
import { createSchemaField, FormConsumer } from '@silver-formily/vue'
import { Button as VanButton, Icon as VanIcon } from 'vant'
import { defineComponent } from 'vue'
import { showDemoResult } from '../shared'
const { SchemaField, SchemaStringField, SchemaVoidField } = createSchemaField({
components: {
FormItem,
FormStep,
Input,
},
})
const form = createForm()
const formStep = FormStep.createFormStep()
const VerifyTitle = defineComponent({
name: 'VerifyTitle',
setup() {
return () => (
<div class="slot-title">
<VanIcon name="shield-o" class="slot-title__icon" />
<span class="slot-title__text">核验资料</span>
</div>
)
},
})
const AddressTitle = defineComponent({
name: 'AddressTitle',
setup() {
return () => (
<div class="slot-title">
<VanIcon name="location-o" class="slot-title__icon" />
<span class="slot-title__text">配送地址</span>
</div>
)
},
})
function createStepIcon(icon: string, modifier: string) {
return defineComponent({
name: `StepIcon${modifier}`,
setup() {
return () => (
<span class={['slot-icon', `slot-icon--${modifier}`]}>
<VanIcon name={icon} class="slot-icon__inner" />
</span>
)
},
})
}
const ActiveIcon = createStepIcon('underway-o', 'active')
const FinishIcon = createStepIcon('passed', 'finish')
const InactiveIcon = createStepIcon('todo-list-o', 'inactive')
async function handleSubmit(values: typeof form.values) {
await showDemoResult(values, '插槽自定义渲染提交结果')
}
</script>
<template>
<Form :form="form">
<SchemaField>
<SchemaVoidField x-component="FormStep" :x-component-props="{ formStep, activeColor: '#1989fa' }">
<SchemaVoidField
x-component="FormStep.StepPane"
:x-component-props="{
title: '资料核验',
}"
:x-content="{
title: VerifyTitle,
}"
>
<SchemaStringField
name="realName"
title="真实姓名"
required
x-decorator="FormItem"
x-component="Input"
:x-component-props="{ placeholder: '请输入真实姓名' }"
/>
<SchemaStringField
name="idNumber"
title="身份证号"
required
x-decorator="FormItem"
x-component="Input"
:x-component-props="{ placeholder: '请输入身份证号' }"
/>
</SchemaVoidField>
<SchemaVoidField
x-component="FormStep.StepPane"
:x-component-props="{
title: '配送地址',
}"
:x-content="{
title: AddressTitle,
activeIcon: ActiveIcon,
finishIcon: FinishIcon,
inactiveIcon: InactiveIcon,
}"
>
<SchemaStringField
name="city"
title="所在城市"
required
x-decorator="FormItem"
x-component="Input"
:x-component-props="{ placeholder: '请输入城市' }"
/>
<SchemaStringField
name="address"
title="详细地址"
required
x-decorator="FormItem"
x-component="Input.TextArea"
:x-component-props="{ rows: 2, placeholder: '请输入街道、楼栋、门牌号' }"
/>
</SchemaVoidField>
<SchemaVoidField
x-component="FormStep.StepPane"
:x-component-props="{
title: '确认提交',
}"
>
<SchemaStringField
name="remark"
title="补充说明"
x-decorator="FormItem"
x-component="Input.TextArea"
:x-component-props="{ rows: 2, placeholder: '例如送达时间、电话备注等' }"
/>
</SchemaVoidField>
</SchemaVoidField>
</SchemaField>
<FormConsumer>
<template #default>
<FormButtonGroup layout="horizontal">
<VanButton
:disabled="!formStep.allowBack"
plain
type="default"
@click="formStep.back()"
>
上一步
</VanButton>
<VanButton
v-if="formStep.allowNext"
type="primary"
@click="formStep.next()"
>
下一步
</VanButton>
<Submit v-else :on-submit="handleSubmit">
提交
</Submit>
</FormButtonGroup>
</template>
</FormConsumer>
</Form>
</template>
<style scoped>
:deep(.slot-title) {
display: inline-flex;
align-items: center;
gap: 6px;
}
:deep(.slot-title__icon) {
color: #1989fa;
font-size: 16px;
}
:deep(.slot-title__text) {
font-weight: 600;
color: var(--van-text-color);
}
:deep(.slot-icon) {
display: inline-flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
border-radius: 50%;
font-size: 12px;
line-height: 1;
}
:deep(.slot-icon__inner) {
font-size: 14px;
}
:deep(.slot-icon--active) {
color: #fff;
background: linear-gradient(135deg, #1989fa 0%, #5ca8ff 100%);
}
:deep(.slot-icon--finish) {
color: #1989fa;
background: rgba(25, 137, 250, 0.12);
}
:deep(.slot-icon--inactive) {
color: var(--van-text-color-3);
background: var(--van-gray-2);
}
</style>API
FormStep
| 属性名 | 类型 | 描述 | 默认值 |
|---|---|---|---|
formStep | IFormStep | 传入通过 FormStep.createFormStep() 创建的步骤模型 | 内部自动创建 |
active | number | string | 受控地指定当前激活步骤;会覆盖内部步骤索引 | - |
hideSteps | boolean | 是否隐藏顶部 Steps 步骤条,仅保留当前步骤内容区 | false |
其余未单独列出的属性会继续透传给 Vant Steps,例如 direction、activeColor、inactiveColor、activeIcon 等。
FormStep.StepPane
StepPane 自身只承担 Schema 容器职责,常用配置约定如下:
x-component-props.title:步骤标题x-content.title:自定义标题内容,可传字符串、数字或组件x-content.icon:统一自定义步骤图标,会同时作为激活、完成和未激活态图标的兜底内容x-content.activeIcon/finishIcon/inactiveIcon:分别细分三种状态的图标内容
除上述字段外,其余 x-component-props 会透传到 Vant Step 根节点属性上。
FormStep.createFormStep
ts
interface createFormStep {
(current?: number): IFormStep
}
interface IFormStep {
current: number
allowNext: boolean
allowBack: boolean
setCurrent: (key: number) => void
submit: Formily.Core.Models.Form['submit']
next: () => void
back: () => void
}useFormStep
ts
interface useFormStep {
(): ComputedRef<IFormStep>
}用于在 FormStep 的子孙组件中读取当前步骤实例。无论这个实例来自外部传入的 formStep,还是组件内部自动创建的默认实例,都可以通过这个 hook 访问到。