Skip to content

Form 表单

由输入框、单选框、复选框、下拉框等控件组成,用以收集、校验、提交数据。

组件注册

js
import { FForm } from '@fesjs/fes-design';

app.use(FForm);

代码演示

基本使用

包括各种表单项,比如输入框、选择器、单选框、多选框等。

play
<template>
    <FForm :labelWidth="100">
        <FFormItem label="输入姓名">
            <FInput placeholder="请输入" />
        </FFormItem>
        <FFormItem label="选择城市">
            <FSelect clearable placeholder="请单选">
                <FOption
                    v-for="(item, index) in optionList"
                    :key="index"
                    :value="item.value"
                    :label="item.label"
                />
            </FSelect>
        </FFormItem>
        <FFormItem label="选择性别">
            <FRadioGroup>
                <FRadio :value="1"></FRadio>
                <FRadio :value="2"></FRadio>
            </FRadioGroup>
        </FFormItem>
        <FFormItem label="这是一段长的表单项描述">
            <FInput placeholder="请输入" />
        </FFormItem>
        <FFormItem label="年龄范围">
            <FCheckboxGroup>
                <FCheckbox :value="1 - 5">1 - 5</FCheckbox>
                <FCheckbox :value="6 - 10">6 - 10</FCheckbox>
                <FCheckbox :value="11 - 15">11 - 15</FCheckbox>
                <FCheckbox :value="16 - 20">16 - 20</FCheckbox>
                <FCheckbox :value="21 - 25">21 - 25</FCheckbox>
                <FCheckbox :value="26 - 30">26 - 30</FCheckbox>
                <FCheckbox :value="31 - 35">31 - 35</FCheckbox>
                <FCheckbox :value="36 - 40">36 - 40</FCheckbox>
                <FCheckbox :value="41 - 45">41 - 45</FCheckbox>
                <FCheckbox :value="36 - 40">36 - 40</FCheckbox>
                <FCheckbox :value="41 - 45">41 - 45</FCheckbox>
                <FCheckbox :value="46 - 50">46 - 50</FCheckbox>
                <FCheckbox :value="51 - 55">51 - 55</FCheckbox>
                <FCheckbox :value="56 - 60">56 - 60</FCheckbox>
                <FCheckbox :value="61 - 65">61 - 65</FCheckbox>
            </FCheckboxGroup>
        </FFormItem>
        <FFormItem label="上传" :contentStyle="{ display: 'block' }">
            <FUpload>
                <template #tip>
                    <div class="f-upload__tip">
                        只能上传 jpg/png 等图片文件,且不超过 5KB
                    </div>
                </template>
            </FUpload>
        </FFormItem>
        <FFormItem label=" ">
            <FButton type="primary">Submit</FButton>
        </FFormItem>
    </FForm>
</template>

<script>
export default {
    setup() {
        return {
            optionList: [
                {
                    value: 'HuNan',
                    label: '湖南',
                },
                {
                    value: 'HuBei',
                    label: '湖北',
                },
                {
                    value: 'ZheJiang',
                    label: '浙江',
                },
                {
                    value: 'GuangDong',
                    label: '广东',
                },
                {
                    value: 'JiangSu',
                    label: '江苏',
                },
            ],
        };
    },
};
</script>

<style scoped></style>

行内表单

当垂直方向空间受限且表单较简单时,可以使用行内表单模式。

play
<template>
    <FForm labelWidth="120px">
        <FFormItem label="行间距:">
            <FInputNumber
                v-model="inlineItemGap"
                :min="5"
                :max="30"
                :step="1"
            />
            <span style="margin-left: 10px">px</span>
        </FFormItem>
        <FFormItem label="每项宽度设置方式:">
            <FRadioGroup
                v-model="itemWidthType"
                :options="[
                    { label: '占据列数', value: 'span' },
                    { label: '固定宽度', value: 'inlineItemWidth' },
                ]"
            />
        </FFormItem>
        <FFormItem v-if="itemWidthType === 'span'" label="占据列数:">
            <FInputNumber
                v-model="span"
                :min="6"
                :max="24"
                :step="1"
            />
            <span style="margin-left: 10px">px</span>
        </FFormItem>
        <FFormItem
            v-if="itemWidthType === 'inlineItemWidth'"
            label="固定宽度:"
        >
            <FInputNumber
                v-model="inlineItemWidth"
                :min="100"
                :max="500"
                :step="10"
            />
            <span style="margin-left: 10px">px</span>
        </FFormItem>
    </FForm>

    <FDivider />

    <FForm
        layout="inline"
        labelWidth="100px"
        labelPosition="right"
        :inlineItemGap="inlineItemGap"
        :span="itemWidthType === 'span' ? span : undefined"
        :inlineItemWidth="
            itemWidthType === 'inlineItemWidth' ? inlineItemWidth : undefined
        "
    >
        <FFormItem label="输入姓名">
            <FInput placeholder="请输入" />
        </FFormItem>
        <FFormItem label="选择城市">
            <FSelect clearable placeholder="请单选">
                <FOption
                    v-for="(item, index) in optionList"
                    :key="index"
                    :value="item.value"
                    :label="item.label"
                />
            </FSelect>
        </FFormItem>
        <FFormItem label="选择城市">
            <FSelect clearable placeholder="请单选">
                <FOption
                    v-for="(item, index) in optionList"
                    :key="index"
                    :value="item.value"
                    :label="item.label"
                />
            </FSelect>
        </FFormItem>
        <FFormItem label="选择性别">
            <FRadioGroup>
                <FRadio :value="0">未知</FRadio>
                <FRadio :value="1"></FRadio>
                <FRadio :value="2"></FRadio>
            </FRadioGroup>
        </FFormItem>
        <FFormItem label="输入其他">
            <FInput placeholder="请输入" />
        </FFormItem>
        <FFormItem label="选择性别" align="center">
            <FRadioGroup>
                <FRadio :value="0">未知</FRadio>
                <FRadio :value="1"></FRadio>
                <FRadio :value="2"></FRadio>
            </FRadioGroup>
        </FFormItem>
    </FForm>
</template>

<script>
import { defineComponent, ref } from 'vue';

export default defineComponent({
    setup() {
        const inlineItemGap = ref(11);
        const itemWidthType = ref('span');
        const span = ref(12);
        const inlineItemWidth = ref(300);

        return {
            inlineItemGap,
            itemWidthType,
            span,
            inlineItemWidth,

            optionList: [
                {
                    value: 'HuNan',
                    label: '湖南湖南湖南湖南湖南湖南湖南',
                },
                {
                    value: 'HuBei',
                    label: '湖北湖北湖北湖北湖北湖北湖北',
                },
                {
                    value: 'ZheJiang',
                    label: '浙江',
                },
                {
                    value: 'GuangDong',
                    label: '广东',
                },
                {
                    value: 'JiangSu',
                    label: '江苏',
                },
            ],
        };
    },
});
</script>

对齐方式

根据具体目标和制约因素,选择最佳的标签对齐方式。

play
<template>
    <FForm labelWidth="150px">
        <FFormItem label="表单布局:">
            <FRadioGroup
                v-model="layout"
                :options="[
                    { label: 'horizontal(默认)', value: 'horizontal' },
                    { label: 'inline', value: 'inline' },
                ]"
            />
        </FFormItem>
        <FFormItem label="标签位置:">
            <FRadioGroup
                v-model="labelPosition"
                :options="[
                    { label: '左对齐(默认)', value: 'left' },
                    { label: '右对齐', value: 'right' },
                    { label: '顶对齐', value: 'top' },
                ]"
            />
        </FFormItem>
        <FFormItem v-if="labelPosition !== 'top'" label="表单项对齐方式:">
            <FRadioGroup
                v-model="align"
                :options="[
                    { label: 'flex-start(默认)', value: 'flex-start' },
                    { label: 'baseline', value: 'baseline' },
                    { label: 'center', value: 'center' },
                ]"
            />
        </FFormItem>
    </FForm>

    <FDivider />

    <div class="tab-content">
        <FForm
            :layout="layout"
            :labelWidth="labelPosition === 'top' ? undefined : 100"
            :labelPosition="labelPosition"
            :span="layout === 'inline' ? 12 : undefined"
            :align="currentAlign"
        >
            <FFormItem label="展示信息">
                <div>这是一段表单项内容信息</div>
            </FFormItem>
            <FFormItem label="姓名">
                <FInput placeholder="请输入姓名" />
            </FFormItem>
            <FFormItem label="手机号码">
                <FInput placeholder="请输入手机号码" />
            </FFormItem>
            <FFormItem label="这是一段长的表单项描述">
                <FInput placeholder="请输入" />
            </FFormItem>
            <FFormItem label="备注">
                <FInput type="textarea" placeholder="请输入备注信息" />
            </FFormItem>
            <FFormItem label="自定义对齐" align="center">
                <FCheckboxGroup>
                    <FCheckbox :value="1 - 5">1 - 5</FCheckbox>
                    <FCheckbox :value="6 - 10">6 - 10</FCheckbox>
                    <FCheckbox :value="11 - 15">11 - 15</FCheckbox>
                    <FCheckbox :value="16 - 20">16 - 20</FCheckbox>
                    <FCheckbox :value="21 - 25">21 - 25</FCheckbox>
                    <FCheckbox :value="26 - 30">26 - 30</FCheckbox>
                    <FCheckbox :value="31 - 35">31 - 35</FCheckbox>
                    <FCheckbox :value="36 - 40">36 - 40</FCheckbox>
                    <FCheckbox :value="41 - 45">41 - 45</FCheckbox>
                    <FCheckbox :value="36 - 40">36 - 40</FCheckbox>
                    <FCheckbox :value="41 - 45">41 - 45</FCheckbox>
                    <FCheckbox :value="46 - 50">46 - 50</FCheckbox>
                    <FCheckbox :value="51 - 55">51 - 55</FCheckbox>
                    <FCheckbox :value="56 - 60">56 - 60</FCheckbox>
                    <FCheckbox :value="61 - 65">61 - 65</FCheckbox>
                </FCheckboxGroup>
            </FFormItem>
        </FForm>
    </div>
</template>

<script>
import { computed, defineComponent, ref } from 'vue';

export default defineComponent({
    setup() {
        const layout = ref('horizontal');
        const labelPosition = ref('right');
        const align = ref('flex-start');

        const currentAlign = computed(() =>
            labelPosition.value !== 'top' ? align.value : undefined,
        );

        return {
            layout,
            labelPosition,
            align,
            currentAlign,
        };
    },
});
</script>

<style scoped>
.tab-content {
    margin-top: 24px;
}
</style>

表单禁用

当需要设置整个表单不可用的时候,可以设置 disabled 属性。表单包裹的组件均会设置为禁用状态。

play
<template>
    <FForm labelWidth="120px">
        <FFormItem label="表单禁用:">
            <FSwitch v-model="formDisabled" />
        </FFormItem>
    </FForm>

    <FDivider />

    <FForm :labelWidth="80" :disabled="formDisabled">
        <FFormItem label="输入框">
            <FInput placeholder="请输入" />
        </FFormItem>
        <FFormItem label="文本输入">
            <FInput placeholder="请输入文本内容" type="textarea" />
        </FFormItem>
        <FFormItem label="数字输入">
            <FInputNumber v-model="numVal" :max="100" />
        </FFormItem>
        <FFormItem label="选择器">
            <FSelect clearable placeholder="请单选">
                <FOption
                    v-for="(item, index) in optionList"
                    :key="index"
                    :value="item.value"
                    :label="item.label"
                />
            </FSelect>
        </FFormItem>
        <FFormItem label="单选框">
            <FRadioGroup>
                <FRadio :value="1"></FRadio>
                <FRadio :value="2"></FRadio>
            </FRadioGroup>
        </FFormItem>
        <FFormItem label="单选按钮组">
            <FRadioGroup v-model="radioBtnVal">
                <FRadioButton :value="1">选项一</FRadioButton>
                <FRadioButton :value="2">选项二</FRadioButton>
                <FRadioButton :value="3">选项三</FRadioButton>
            </FRadioGroup>
        </FFormItem>
        <FFormItem label="复选框">
            <FCheckboxGroup>
                <FCheckbox :value="1 - 10">1-10</FCheckbox>
                <FCheckbox :value="11 - 30">11-30</FCheckbox>
                <FCheckbox :value="31 - 60">31-60</FCheckbox>
            </FCheckboxGroup>
        </FFormItem>
        <FFormItem label="开关">
            <FSwitch v-model="toggleVal" />
        </FFormItem>
        <FFormItem label="时间选择">
            <FTimePicker
                style="width: 200px"
                modelValue="22:22:22"
                placeholder="请输入"
            />
        </FFormItem>
        <FFormItem label="日期选择">
            <FDatePicker type="year" placeholder="选择年份" />
        </FFormItem>
        <FFormItem label="上传">
            <FUpload />
        </FFormItem>
        <FFormItem label="拖拽上传">
            <FUpload>
                <FUploadDragger> 点击或者拖拽文件到此区域 </FUploadDragger>
            </FUpload>
        </FFormItem>
        <FFormItem label="树形选择器">
            <FSelectTree />
        </FFormItem>
        <FFormItem label="级联选择器">
            <FSelectCascader />
        </FFormItem>
        <FFormItem label=" ">
            <FButton type="primary">Submit</FButton>
        </FFormItem>
    </FForm>
</template>

<script>
import { ref } from 'vue';

export default {
    setup() {
        const formDisabled = ref(true);
        const toggleVal = ref();
        const numVal = ref(0);
        const radioBtnVal = ref(0);
        return {
            optionList: [
                {
                    value: 'HuNan',
                    label: '湖南',
                },
                {
                    value: 'HuBei',
                    label: '湖北',
                },
                {
                    value: 'ZheJiang',
                    label: '浙江',
                },
                {
                    value: 'GuangDong',
                    label: '广东',
                },
                {
                    value: 'JiangSu',
                    label: '江苏',
                },
            ],
            toggleVal,
            numVal,
            formDisabled,
            radioBtnVal,
        };
    },
};
</script>

<style scoped></style>

常规表单验证

Form 组件提供表单验证的功能,通过 rules 属性传入约定的验证规则,并将 FormItem 的 prop 属性设置为需校验的字段名即可。表单验证目的在于尽可能让用户更早地发现并纠正错误。

play
<template>
    <FForm labelWidth="150px">
        <FFormItem label="表单禁用:">
            <FSwitch v-model="formDisabled" />
        </FFormItem>
        <FFormItem label="是否显示校验错误信息:">
            <FSwitch v-model="showMessage" />
        </FFormItem>
    </FForm>

    <FDivider />

    <FForm
        ref="formRef"
        labelWidth="140px"
        labelPosition="right"
        :model="modelForm"
        :rules="rules"
        :disabled="formDisabled"
        :showMessage="showMessage"
    >
        <FFormItem prop="name">
            <template #label><span>姓名(slot)</span></template>
            <FSpace>
                <FInput
                    v-model="modelForm.name.first"
                    placeholder="请输入first name"
                    @input="changeHandler"
                />
                <FInput
                    v-model="modelForm.name.last"
                    placeholder="请输入last name"
                    @input="changeHandler"
                />
            </FSpace>
        </FFormItem>
        <FFormItem
            label="年龄"
            prop="age"
            :rules="[{ required: true, type: 'number', message: '请输入年龄' }]"
        >
            <FInputNumber
                v-model="modelForm.age"
                placeholder="请输入年龄"
            />
        </FFormItem>
        <FFormItem label="地址单选" prop="sregion">
            <FSelect
                v-model="modelForm.sregion"
                clearable
                placeholder="请单选"
                @change="changeHandler"
            >
                <FOption
                    v-for="(item, index) in optionList"
                    :key="index"
                    :value="item.value"
                    :label="item.label"
                />
            </FSelect>
        </FFormItem>
        <FFormItem label="地址多选" prop="mregion">
            <FSelect
                v-model="modelForm.mregion"
                multiple
                placeholder="请多选"
                @change="changeHandler"
            >
                <FOption
                    v-for="(item, index) in optionList"
                    :key="index"
                    :value="item.value"
                    :label="item.label"
                />
            </FSelect>
        </FFormItem>
        <FFormItem label="选择时间" prop="time">
            <FTimePicker
                v-model="modelForm.time"
                placeholder="请输入时间"
                format="HH:mm"
            />
        </FFormItem>
        <FFormItem label="选择性别" prop="sex">
            <FRadioGroup v-model="modelForm.sex" @change="changeHandler">
                <FRadio value="1"></FRadio>
                <FRadio value="2"></FRadio>
            </FRadioGroup>
        </FFormItem>
        <FFormItem label="级联单选" prop="singleCity">
            <FSelectCascader
                v-model="modelForm.singleCity"
                :data="cascaderOptions"
                @change="changeHandler"
            />
        </FFormItem>
        <FFormItem label="级联多选" prop="multiCity">
            <FSelectCascader
                v-model="modelForm.multiCity"
                :data="cascaderOptions"
                :multiple="true"
                @change="changeHandler"
            />
        </FFormItem>
        <FFormItem
            label="备注 slot"
            labelClass="more-label-container"
            prop="desc"
        >
            <template #label>
                <span class="more-label-text" @click="descClickHandler">
                    <QuestionCircleFilled /> 备注(slot)
                </span>
            </template>
            <FInput
                v-model="modelForm.desc"
                type="textarea"
                placeholder="请输入备注信息,以填入的【姓名】开头"
            />
        </FFormItem>
        <FFormItem label=" ">
            <FSpace>
                <FButton type="primary" @click="submitHandler"> 提交 </FButton>
                <FButton type="primary" @click="clearHandler"> 清除 </FButton>
                <FButton type="primary" @click="resetHandler">重置</FButton>
            </FSpace>
        </FFormItem>
    </FForm>
</template>

<script>
import { computed, reactive, ref } from 'vue';
import { FMessage } from '@fesjs/fes-design';

export default {
    setup() {
        const formRef = ref(null);
        const formDisabled = ref(false);
        const showMessage = ref(true);

        const modelForm = reactive({
            name: {
                first: '',
                last: '',
            },
            sregion: '',
            mregion: ['HuNan', 'HuBei'],
            time: '',
            sex: '2',
            show: true,
            permission: [],
            desc: '',
            singleCity: '',
            multiCity: [],
        });

        const validateContFun = (rule, value) => {
            return Boolean(value.startsWith(modelForm.name.first));
        };

        const rules = computed(() => {
            return {
                name: {
                    type: 'object',
                    required: true,
                    fields: {
                        first: {
                            type: 'string',
                            required: true,
                            min: 3,
                            max: 8,
                            message: 'first姓名长度在 3 到 8 个字符',
                            trigger: 'change',
                        },
                        last: {
                            type: 'string',
                            required: true,
                            min: 3,
                            max: 8,
                            message: 'last姓名长度在 3 到 8 个字符',
                            trigger: 'change',
                        },
                    },
                },
                sregion: [
                    {
                        required: true,
                        message: '请选择单选',
                        trigger: ['change', 'blur'],
                    },
                ],
                mregion: [
                    {
                        required: true,
                        type: 'array',
                        message: '请选择多选',
                        trigger: ['change', 'blur'],
                    },
                ],
                time: [
                    {
                        required: true,
                        message: '请选择时间',
                        trigger: ['change', 'blur'],
                    },
                ],
                sex: [
                    {
                        required: true,
                        message: '请选择性别',
                        trigger: 'change',
                    },
                ],
                permission: [
                    {
                        required: true,
                        message: '请选择权限',
                        trigger: 'change',
                        type: 'array',
                    },
                ],
                desc: [
                    {
                        min: 3,
                        max: 8,
                        message: '长度在 3 到 8 个字符',
                        trigger: ['change'],
                    },
                    {
                        validator: validateContFun,
                        message: '请输入以【姓名】开头的备注信息',
                        trigger: ['change'],
                    },
                ],
                singleCity: [
                    {
                        required: true,
                        message: '请选择单选',
                        trigger: ['change', 'blur'],
                    },
                ],
                multiCity: [
                    {
                        required: true,
                        message: '请选择多选',
                        trigger: ['change', 'blur'],
                        type: 'array',
                    },
                ],
            };
        });

        const optionList = [
            {
                value: 'HuNan',
                label: '湖南',
            },
            {
                value: 'HuBei',
                label: '湖北',
            },
            {
                value: 'ZheJiang',
                label: '浙江',
            },
            {
                value: 'GuangDong',
                label: '广东',
            },
            {
                value: 'JiangSu',
                label: '江苏',
            },
        ];
        const cascaderOptions = [
            {
                value: '110000',
                label: '北京市',
                children: [
                    {
                        value: '110100',
                        label: '市辖区',
                        children: [
                            {
                                value: '110101',
                                label: '东城区东城区东城区东城区东城区东城区',
                            },
                            {
                                value: '110102',
                                label: '西城区',
                            },
                        ],
                    },
                    {
                        value: '110200',
                        label: '市辖县',
                        children: [
                            {
                                value: '110228',
                                label: '密云县',
                            },
                            {
                                value: '110229',
                                label: '延庆县',
                            },
                        ],
                    },
                ],
            },
            {
                value: '130000',
                label: '河北省',
                children: [
                    {
                        value: '130100',
                        label: '石家庄市',
                    },
                    {
                        value: '130200',
                        label: '唐山市',
                    },
                ],
            },
            {
                value: '140000',
                label: '山西省',
            },
        ];

        const changeHandler = (value) => {
            console.log('[form.validate] [changeHandler] value:', value);
        };

        const submitHandler = async () => {
            try {
                await formRef.value.validate();
                console.log('[form.validate] [submitHandler] 表单验证成功~');
            } catch (error) {
                console.log(
                    '[form.validate] [submitHandler] 表单验证失败, error:',
                    error,
                );
                FMessage.warn('请检查表单项');
            }
        };
        const clearHandler = () => {
            formRef.value.clearValidate();
        };
        const resetHandler = () => {
            formRef.value.resetFields();
        };

        const descClickHandler = () => {
            FMessage.success({ content: '你点击了备注<slot/>!' });
        };

        return {
            formRef,
            formDisabled,
            showMessage,
            modelForm,
            rules,
            optionList,
            changeHandler,
            submitHandler,
            clearHandler,
            resetHandler,
            descClickHandler,
            cascaderOptions,
        };
    },
};
</script>

<style scoped>
.more-label-text {
    color: #9e9e9e;
    display: flex;
    align-items: center;
}
.more-label-text > :first-child {
    margin-right: 5px;
}
</style>

复杂表单验证

复杂表单校验场景:自定义规则、联动校验、动态组件单个选项/整体校验。

play
<template>
    <FForm
        ref="formRef"
        labelWidth="140px"
        labelPosition="right"
        :model="modelForm"
        :rules="rules"
    >
        <div class="complex-validate-item">
            <MoreCircleFilled /> 自定义 trigger、validator 场景:
        </div>
        <FFormItem label="密码" prop="password">
            <FInput
                v-model="modelForm.password"
                type="password"
                showPassword
                placeholder="请输入密码"
                @input="handlePasswordInput"
            />
        </FFormItem>
        <FFormItem ref="rePasswordRef" label="再次输入" prop="rePassword">
            <FInput
                v-model="modelForm.rePassword"
                type="password"
                showPassword
                placeholder="请再次输入密码"
            />
        </FFormItem>

        <div class="complex-validate-item">
            <MoreCircleFilled /> 联动校验场景:
        </div>
        <FFormItem label="类型选择" prop="type">
            <FRadioGroup v-model="modelForm.type">
                <FRadio value="admin">admin</FRadio>
                <FRadio value="edit">edit</FRadio>
                <FRadio value="run">run</FRadio>
            </FRadioGroup>
        </FFormItem>
        <!-- 在 FForm 维度【全局rules】绑定联动规则  -->
        <FFormItem
            v-if="modelForm.type === 'admin'"
            label="admin 详情"
            prop="adminDesc"
        >
            <FInput
                v-model="modelForm.adminDesc"
                placeholder="请输入adminDesc【在 FForm 维度绑定联动规则】"
            />
        </FFormItem>
        <!-- 在 FFormItem 维度绑定联动规则  -->
        <FFormItem
            v-if="modelForm.type === 'edit'"
            label="edit 详情"
            prop="editDesc"
            :rules="
                modelForm.type === 'edit'
                    ? [{ required: true, message: '请输入editDesc' }]
                    : []
            "
        >
            <FInput
                v-model="modelForm.editDesc"
                placeholder="请输入editDesc【在 FFormItem 维度绑定联动规则】"
            />
        </FFormItem>

        <div class="complex-validate-item">
            <MoreCircleFilled /> v-for 任意添加选项场景校验:
        </div>
        <FFormItem
            v-for="(item, index) in modelForm.options"
            :key="index"
            :label="item.label"
            :prop="`options[${index}].value`"
            :rules="[{ required: index % 2 !== 0, message: '请输入选项值' }]"
        >
            <FSpace>
                <FInput
                    v-model="modelForm.options[index].value"
                    placeholder="请输入选项"
                />
                <PlusSquareOutlined @click="addOptionItem" />
            </FSpace>
        </FFormItem>

        <div class="complex-validate-item">
            <MoreCircleFilled /> v-for 动态 ref 校验:
        </div>
        <div
            v-for="(item, index) in modelForm.scenes"
            :key="index"
            style="display: flex; margin-left: 65px"
        >
            <FFormItem
                :prop="`scenes[${index}].name`"
                :label="`场景${index + 1}名称:`"
                labelWidth="80px"
            >
                <FInput
                    v-model="modelForm.scenes[index].name"
                    :maxlength="10"
                    @input="validateScenesCont(index)"
                />
            </FFormItem>
            <FFormItem
                :ref="(el) => (sceneFormItemRefList[index] = el)"
                label="内容:"
                labelWidth="80px"
                :prop="`scenes[${index}].content`"
                :rules="
                    modelForm.scenes[index].name
                        ? [{ required: true, message: '' }]
                        : []
                "
            >
                <FInput
                    v-model="modelForm.scenes[index].content"
                    :maxlength="10"
                    :placeholder="`名称${index + 1}填写,内容必填`"
                />
            </FFormItem>
        </div>

        <FFormItem label=" ">
            <FSpace>
                <FButton type="primary" @click="submitHandler"> 提交 </FButton>
                <FButton type="primary" @click="clearHandler"> 清除 </FButton>
                <FButton type="primary" @click="resetHandler">重置</FButton>
            </FSpace>
        </FFormItem>
    </FForm>
</template>

<script>
import { computed, reactive, ref } from 'vue';

export default {
    setup() {
        const formRef = ref(null);
        const rePasswordRef = ref(null);
        const modelForm = reactive({
            password: '',
            rePassword: '',
            type: 'admin',
            typeDesc: '',
            options: [{ label: '选项1', value: '' }],
            scenes: [
                { name: '', content: '' },
                { name: '', content: '' },
            ],
        });
        const sceneFormItemRefList = ref([]);

        const validatePasswordStartWith = (rule, value) => {
            return Boolean(
                modelForm.password
                && modelForm.password.startsWith(value)
                && modelForm.password.length >= value.length,
            );
        };
        const rules = computed(() => {
            return {
                password: [
                    {
                        required: true,
                        message: '请输入密码',
                        trigger: ['blur', 'change'],
                    },
                ],
                rePassword: [
                    {
                        required: true,
                        message: '请再次输入密码',
                        trigger: ['change', 'blur'],
                    },
                    {
                        validator: validatePasswordStartWith,
                        message: '再次输入密码时,两次密码输入不一致',
                        trigger: ['change'],
                    },
                    {
                        validator: (rule, value) =>
                            value === modelForm.password,
                        message: '输入密码时,两次密码输入不一致',
                        trigger: 'password-input',
                    },
                ],
                adminDesc: [
                    modelForm.type === 'admin' && {
                        required: true,
                        message: '请输入adminDesc',
                        trigger: ['change', 'blur'],
                    },
                ],
            };
        });

        // 调用自定义的 password-input trigger
        const handlePasswordInput = () => {
            if (modelForm.rePassword) {
                rePasswordRef.value.validate('password-input');
            }
        };

        const addOptionItem = () => {
            modelForm.options.push({
                label: `选项${modelForm.options.length + 1}`,
                value: '',
            });
        };

        const validateScenesCont = (index) => {
            // 通过 FFormItem 的动态 ref 调用 validate 校验
            modelForm?.scenes[index]?.name
                ? sceneFormItemRefList.value[index].validate()
                : sceneFormItemRefList.value[index].clearValidate();
        };

        const submitHandler = async () => {
            // 通过 FForm 的 validate 直接校验
            try {
                await formRef.value.validate();
                console.log(
                    '[form.complexValidate] [submitHandler] 表单验证成功',
                );
            } catch (error) {
                console.log(
                    '[form.complexValidate] [submitHandler] 表单验证失败, error:',
                    error,
                );
            }
        };
        const clearHandler = () => {
            formRef.value.clearValidate();
        };
        const resetHandler = () => {
            formRef.value.resetFields();
        };

        return {
            formRef,
            rePasswordRef,
            modelForm,
            rules,
            sceneFormItemRefList,

            handlePasswordInput,
            validateScenesCont,
            addOptionItem,
            submitHandler,
            clearHandler,
            resetHandler,
        };
    },
};
</script>

<style scoped>
.complex-validate-item {
    margin: 10px 0 20px 60px;
    color: rgb(136 136 136);
    display: flex;
    align-items: center;
}
.complex-validate-item > :first-child {
    margin-right: 5px;
}
</style>

异步表单验证

play
<template>
    <FForm
        ref="formRef"
        labelWidth="140px"
        labelPosition="right"
        :model="modelForm"
        :rules="rules"
    >
        <FFormItem prop="name" label="实时校验">
            <FInput
                v-model="modelForm.name"
                placeholder="请输入姓名"
                :maxlength="30"
                showWordLimit
            />
            <LoadingOutlined
                v-show="modelForm.nameLoading"
                style="margin-left: 10px"
            />
        </FFormItem>
        <FFormItem prop="phone" label="防抖校验">
            <FInput
                v-model="modelForm.phone"
                placeholder="请输入手机号"
                :maxlength="11"
                showWordLimit
            />
            <LoadingOutlined
                v-show="modelForm.phoneLoading"
                style="margin-left: 10px"
            />
        </FFormItem>
        <FFormItem label=" ">
            <FSpace>
                <FButton
                    type="primary"
                    :loading="modelForm.submitLoading"
                    @click="submitHandler"
                >
                    {{ modelForm.submitText }}
                </FButton>
                <FButton type="primary" @click="clearHandler"> 清除 </FButton>
                <FButton type="primary" @click="resetHandler">重置</FButton>
            </FSpace>
        </FFormItem>
    </FForm>
</template>

<script>
import { computed, reactive, ref } from 'vue';
import { debounce } from 'lodash-es';
import { FMessage } from '@fesjs/fes-design';

function sleep(time) {
    return new Promise((resolve) => setTimeout(resolve, time));
}

export default {
    setup() {
        const formRef = ref(null);

        const modelForm = reactive({
            submitLoading: false,
            submitText: '提交',
            name: '',
            nameLoading: false,
            phone: '',
            phoneLoading: false,
        });

        const debounceValidator = debounce(async (value, resolve, reject) => {
            console.log(
                '[form.asyncValidator] [debounceValidator] phone:',
                value,
            );
            modelForm.phoneLoading = true;
            await sleep(2000);
            if (value.length < 11) {
                reject('异步校验手机号不完整');
            } else {
                resolve();
            }
            modelForm.phoneLoading = false;
        }, 300);

        const rules = computed(() => {
            return {
                name: [
                    {
                        required: true,
                        message: '姓名不能为空',
                    },
                    {
                        asyncValidator: (rule, value) => {
                            return new Promise(async (resolve, reject) => {
                                if (!value) {
                                    return reject('异步校验姓名不能为空');
                                }
                                console.log(
                                    '[form.asyncValidator] [asyncValidator] name:',
                                    value,
                                );

                                modelForm.nameLoading = true;
                                await sleep(3000);
                                modelForm.nameLoading = false;

                                if (value.length < 3) {
                                    return reject('异步校验姓名字符个数不足');
                                } else {
                                    return resolve();
                                }
                            });
                        },
                        message: '姓名至少三个字符长度',
                        trigger: ['change'],
                    },
                ],
                phone: [
                    {
                        required: true,
                        message: '手机号不能为空',
                    },
                    {
                        asyncValidator: (rule, value) => {
                            return new Promise(async (resolve, reject) => {
                                if (!value) {
                                    return reject('异步校验手机号不能为空');
                                }
                                debounceValidator(value, resolve, reject);
                            });
                        },
                        message: '手机号不完整',
                        trigger: ['change'],
                    },
                ],
            };
        });

        const submitHandler = async () => {
            try {
                modelForm.submitLoading = true;
                modelForm.submitText = '校验中';
                await formRef.value.validate();
                console.log(
                    '[form.asyncValidator] [submitHandler] 表单验证成功',
                );
            } catch (error) {
                console.log(
                    '[form.asyncValidator] [submitHandler] 表单验证失败, error:',
                    error,
                );
                FMessage.warn('请检查表单项');
            } finally {
                modelForm.submitLoading = false;
                modelForm.submitText = '提交';
            }
        };
        const clearHandler = () => {
            formRef.value.clearValidate();
        };
        const resetHandler = () => {
            formRef.value.resetFields();
        };

        return {
            formRef,
            modelForm,
            rules,
            submitHandler,
            clearHandler,
            resetHandler,
        };
    },
};
</script>

<style scoped></style>

嵌套验证

play
<template>
    <FForm
        ref="formRef"
        labelWidth="140px"
        labelPosition="right"
        :model="modelForm"
        :rules="rules"
    >
        <FFormItem
            prop="name"
            label="表单项嵌套:"
            :showMessage="false"
            style="margin-bottom: 0"
        >
            <FSpace>
                <FFormItem prop="name.first" :rules="rules.name.fields.first">
                    <FInput
                        v-model="modelForm.name.first"
                        placeholder="请输入FirstName"
                    />
                </FFormItem>
                <FFormItem prop="name.last" :rules="rules.name.fields.last">
                    <FInput
                        v-model="modelForm.name.last"
                        placeholder="请输入LastName"
                    />
                </FFormItem>
            </FSpace>
        </FFormItem>
        <FFormItem prop="userList" label="子表单嵌套:">
            <FSpace vertical style="width: 100%">
                <FButton @click="() => handleAddUser()"> 添加 </FButton>

                <FCard v-for="(item, index) in modelForm.userList" :key="index">
                    <FForm
                        :ref="(el) => (userFormItemRefList[index] = el)"
                        :model="item"
                        :rules="subFormRules"
                    >
                        <FFormItem prop="name" label="姓名:">
                            <FSpace>
                                <FInput
                                    v-model="item.name.first"
                                    placeholder="请输入FirstName"
                                />
                                <FInput
                                    v-model="item.name.last"
                                    placeholder="请输入LastName"
                                />
                            </FSpace>
                        </FFormItem>
                        <FFormItem prop="desc" label="备注:">
                            <FInput
                                v-model="item.desc"
                                placeholder="请输入备注"
                            />
                        </FFormItem>
                    </FForm>
                </FCard>
            </FSpace>
        </FFormItem>

        <FFormItem label=" ">
            <FSpace>
                <FButton type="primary" @click="submitHandler"> 提交 </FButton>
                <FButton type="primary" @click="clearHandler"> 清除 </FButton>
                <FButton type="primary" @click="resetHandler">重置</FButton>
            </FSpace>
        </FFormItem>
    </FForm>
</template>

<script>
import { computed, reactive, ref } from 'vue';
import { FMessage } from '@fesjs/fes-design';

export default {
    setup() {
        const formRef = ref(null);
        const userFormItemRefList = ref([]);

        const modelForm = reactive({
            name: {
                first: '',
                last: '',
            },
            userList: [],
        });

        const rules = computed(() => {
            return {
                name: {
                    type: 'object',
                    required: true,
                    fields: {
                        first: [
                            {
                                type: 'string',
                                required: true,
                                min: 3,
                                max: 8,
                                message: 'first姓名长度在 3 到 8 个字符',
                                trigger: 'change',
                            },
                        ],
                        last: [
                            {
                                type: 'string',
                                required: true,
                                min: 3,
                                max: 8,
                                message: 'last姓名长度在 3 到 8 个字符',
                                trigger: 'change',
                            },
                        ],
                    },
                },
                userList: {
                    type: 'array',
                    required: true,
                    message: '用户列表不能为空',
                    trigger: 'change',
                },
            };
        });

        const subFormRules = computed(() => {
            return {
                name: {
                    type: 'object',
                    required: true,
                    fields: {
                        first: [
                            {
                                type: 'string',
                                required: true,
                                min: 3,
                                max: 8,
                                message: 'first姓名长度在 3 到 8 个字符',
                                trigger: 'change',
                            },
                        ],
                        last: [
                            {
                                type: 'string',
                                required: true,
                                min: 3,
                                max: 8,
                                message: 'last姓名长度在 3 到 8 个字符',
                                trigger: 'change',
                            },
                        ],
                    },
                },
            };
        });

        const handleAddUser = async () => {
            modelForm.userList.push({
                name: {
                    first: '',
                    last: '',
                },
                desc: '',
            });
            // 触发单个表单项的校验
            await formRef.value.validate(['userList']);
        };

        const submitHandler = async () => {
            try {
                await formRef.value.validate();

                for (const userFormItemRef of userFormItemRefList.value) {
                    await userFormItemRef?.validate();
                }

                console.log(
                    '[form.subFormValidator] [submitHandler] 表单验证成功 || modelForm:',
                    modelForm,
                );
            } catch (error) {
                console.log(
                    '[form.subFormValidator] [submitHandler] 表单验证失败, error:',
                    error,
                );
                FMessage.warn('请检查表单项');
            }
        };
        const clearHandler = async () => {
            formRef.value.clearValidate();
            for (const userFormItemRef of userFormItemRefList.value) {
                await userFormItemRef?.clearValidate();
            }
        };
        const resetHandler = async () => {
            formRef.value.resetFields();
            for (const userFormItemRef of userFormItemRefList.value) {
                await userFormItemRef?.resetFields();
            }
        };

        return {
            formRef,
            userFormItemRefList,
            modelForm,
            rules,
            submitHandler,
            clearHandler,
            resetHandler,
            subFormRules,
            handleAddUser,
        };
    },
};
</script>

<style scoped></style>

Form Props

属性说明类型默认值
model表单数对象object-
rules表单验证规则,可查看Form-Item Rule Typeobject-
layout表单布局,可选值为horizontalinlinestringhorizontal
inlineItemWidth仅在 inline 表单中有效。统一定义 FormItem 固定宽度string、number-
inlineItemGap仅在 inline 表单中有效。统一定义整行 FormItem 的间距string、number11px
span仅在 inline 表单中有效。统一定义 FormItem 占据列数,共 24 列number6
labelPosition表单域标签的位置,可选值为lefttoprightstringleft
labelWidth表单域标签的宽度。作为 Form 直接子元素的 form-item 会继承该值。string(100px)、number(100)-
labelClass表单域标签自定义 classstring-
showMessage是否显示校验错误信息booleantrue
disabled表单是否可用booleanfalse
align仅在 labelPositionleftright 时有效。垂直排列方式,可选值为 flex-startbaselinecenterstringflex-start

Form Methods

方法名称说明参数
validate对整体表单、部分表单(传入fieldProps数组)进行校验,返回一个 promise。校验失败时,返回 validvalueserrorFields 信息,其中 valid 表示校验结果,values 表示包含未校验通过的字段,errorFields 表示错误信息(fieldProps?: []) => Promise()
clearValidate移除表单项的校验结果-
resetFields对整个表单进行重置,将所有字段值重置为初始值并移除校验结果-

Form Events

事件名称说明回调参数
submit表单原生提交事件触发(event) => void

Form-Item Props

属性说明类型默认值
prop表单域 model 字段,在使用 validateresetFields 方法的情况下,该属性是必填的string-
value表单项的值。如果存在则优先取该属性string-
rules表单项验证规则,可查看Form-Item Rule TypeArray-
span仅在 inline 表单中有效。自定义 FormItem 占据列数,共 24 列number-
label标签文本string-
labelWidth表单项标签的宽度string(100px)、number(100)-
labelClass表单项标签自定义 classstring-
showMessage是否显示校验错误信息booleantrue
align仅在 labelPositionleftright 时有效。垂直排列方式,可选值为 flex-startbaselinecenterstringflex-start

Form-Item Slots

名称说明
label自定义表单项标签文本

Form-Item Methods

方法名称说明参数
validate验证表单项。如果设定 trigger,该表项指定 trigger 规则会被使用;未设定 trigger,该表项所规则会被使用。shouldRuleBeApplied 可以用来进一步过滤已经经过 trigger 筛选的规则(trigger) => Promise()

Form-Item Rule Type

以下并不是规则的全部用法,如果你想了解更多的用法,请参考 async-validator

属性说明类型默认值
trigger校验触发的时机string、Array-
required是否必填booleanfalse
message校验失败时展示的信息string-
type内建校验类型,可选项 【注意: 非string类型都需要指明 type】stringstring
min最小长度number-
max最大长度number-
validator自定义校验【注意,callback 必须被调用】function(rule, value, callback)-