Vue+Element Plus实现自定义表单弹窗

目录

一、基本框架

1.父组件index.vue

2.子组件FormPop.vue

二、细节补充

[1)input、textarea、select、input number](#1)input、textarea、select、input number)

2)daterange、date、monthrange

3)数据定义

4)没改样式的效果

5)最终效果

三、最终代码

1.父组件index.vue

2.子组件FormPop.vue

3.样式文件FormPop.css


一、基本框架

根据目前的Element Plus以及Vue的父子组件传递数据的方式,简单搭个框架。

(虽然是用的Vue3,但由于个人习惯,还是按选项式写法写的)

1.父组件index.vue

首先父组件index.vue:

复制代码
<template>
    <div class="container">
        <el-button @click="showFormPop">打开表单弹框</el-button>
    </div>
    <FormPop v-if="dialogFormVisible" :formItems="formItems" @closeCallBack="dialogFormVisible = false"
        @submitCallBack="handleConfirm">
    </FormPop>
</template>
  
<script>
import FormPop from "../../components/FormPop/index.vue";

export default {
    name: "index",
    components: {
        FormPop
    },
    data() {
        return {
            dialogFormVisible: false,
        }
    },
    methods: {
        showFormPop() {
            console.log("弹窗显示")
            this.dialogFormVisible = true;
        },
        // 取消对话框弹窗
        handleCancel() {
            this.dialogFormVisible = false;
            console.log("弹窗取消")
        },
        handleConfirm(formData) {
            // 处理弹窗提交
            this.dialogFormVisible = false;
            console.log("弹窗提交成功")
            console.log(formData)
        }
    }
};
</script>

代码含义:

父组件屏幕中只有一个按钮,点击按钮触发showFormPop事件,用于改变dialogFormVisible的值,来控制子组件弹窗是否显示

引入子组件FormPop.vue,并向子组件通过 props 传递数据 formItems,用于定义表单里的内容。

closeCallBack和submitCallBack分别是子组件中点击关闭和点击提交按钮对应的回调函数,在子组件中用emit发送,父组件中监听。

2.子组件FormPop.vue

子组件先搭个框架:

复制代码
<template>
    <el-dialog title="表单弹窗" v-model="dialogFormVisible" width="70%" :before-close="cancelClick">
        <div class="form-part">
            <el-form :model="formData" ref="form">
                <template v-for="(item, index) in formItems" :key="index">
                    <-- 自定义表单内容 -->
                </template>
                <div class="footer">
                    <el-button type="primary" @click="submitForm">提交</el-button>
                    <el-button @click="resetForm">重置</el-button>
                </div>
            </el-form>
        </div>
    </el-dialog>
</template>
  
<script>

export default {
    name: "Index",
    data() {
        return {
            dialogFormVisible: true,
        };
    },
    created() {
        console.log("created", this.formItems;
    },
    props: {
        formItems: {
            type: Array,
            default: () => []
        }
    },
    methods: {
        cancelClick() {
            this.$emit("closeCallBack", false);
        },

        getFormData() {
            return this.$refs.form.validate();
        },

        submitForm() {
            this.getFormData().then(() => {
                this.$emit("submitCallBack", this.formData);
            }).catch(() => {
                console.log("error");
            });
        },

        resetForm() {
            this.$refs.form.resetFields();
        },
    },
}
</script>
  
<style scoped>
@import '../../styles/FormPop.css';
</style>

父子组件中搭好了框架,表单如何显示就看 formItems 中的数据如何定义。

二、细节补充

表单常用的有 input文本输入框、textarea文本域、select下拉选择框、input number数字输入框、日期时间选择器等,以下就是常见样式的子组件内容。

1)input、textarea、select、input number

这几个比较相似所以放在一起。

复制代码
<el-form-item 
                        v-if="['input', 'textarea', 'select', 'rangeInput'].includes(item.type)"
                        :label="item.label" 
                        :prop="item.prop" 
                        :rules="item.rules" 
                        class="form-item">
                        <div v-if="item.type === 'input' || item.type === 'textarea'">
                            <el-input 
                                v-model="formData[item.prop]"
                                :disabled="item.disabled || false" 
                                :clearable="true"
                                :type="item.type" :placeholder="item.disabled ? item.value : (item.placeholder || '请输入')" />
                            <template v-if="item.inputButtonShow">
                                <el-button plain 
                                    :type="item.inputButtonType" 
                                    @click="handleInputButtonClick(item, index)">
                                    {{ item.inputButtonText }}
                                </el-button>
                            </template>
                        </div>
                        <el-select v-if="item.type === 'select'" 
                            v-model="formData[item.prop]" 
                            :clearable="true"
                            :placecholder="item.placeholder || '请选择'">
                            <el-option v-for="(option) in item.options" 
                                :key="'item-' + option.value || option.id"
                                :label="option.label || option.name" 
                                :value="option.value || option.id" />
                        </el-select>
                        <template v-if="item.type === 'rangeInput'">
                            <div class="range-input">
                                <el-input-number 
                                    :min="item.min || 0" :max="item.max || 100"
                                    v-model="formData[item.prop[0]]" 
                                    :disabled="item.disabled || false">
                                </el-input-number>
                                <span class="dash">~</span>
                                <el-input-number 
                                    :min="item.min || 0" :max="item.max || 100"
                                    v-model="formData[item.prop[1]]" 
                                    :disabled="item.disabled || false">
                                </el-input-number>
                            </div>
                        </template>
                    </el-form-item>

以上代表对一个type为'input', 'textarea', 'select', 'rangeInput'四个中的任意一种的创建el-form-item的方式,通过item.type区分。

将label与父组件传递的formItems中的每个item的label绑定,prop与prop绑定等等。

对于input额外增加了紧跟在input输入框后的按钮,(有时候会有input框右侧带个"选择"的按钮的需求,供用户选择,选择后将选中的值更新在input框之类的)用v-if判断item.inputButtonShow的值是否为真,如果为真则右侧显示按钮,为假则只有input框。

2)daterange、date、monthrange

这几个定义比较相近,放在一起。

复制代码
<el-form-item v-if="['dateRange', 'date', 'monthRange'].includes(item.type)" 
                        :label="item.label"
                        :prop="item.prop" 
                        :rules="item.rules" 
                        class="form-item">
                        <el-date-picker v-if="item.type === 'date'" 
                            v-model="formData[item.prop]" 
                            type="date"
                            :placeholder="item.placeholder || '请选择日期'" 
                            clearable 
                            format="YYYY-MM-DD"
                            value-format="YYYY-MM-DD" 
                            :disabled="item.disabled || false">
                        </el-date-picker>
                        <el-date-picker v-if="item.type === 'dateRange'" 
                            v-model="formData[item.prop]" type="daterange"
                            unlink-panels 
                            clearable 
                            range-separator="-" 
                            start-placeholder="开始日期" 
                            end-placeholder="结束日期"
                            :disabled="item.disabled || false">
                        </el-date-picker>
                        <el-date-picker v-if="item.type === 'monthRange'" 
                            v-model="formData[item.prop]" type="monthrange"
                            unlink-panels 
                            clearable 
                            range-separator="-" 
                            start-placeholder="开始月份" 
                            end-placeholder="结束月份"
                            :disabled="item.disabled || false">
                        </el-date-picker>
                    </el-form-item>

同样的用v-if来区分是哪种类型,其余的自行设置,定义方法和Element Plus基本一样的。

3)数据定义

最后设置一下父组件中的formItems,根据不同的类型需要的键值对来定义:

(在电话号码的验证规则中加了正则表达式的验证)

复制代码
            formItems: [
                {
                    label: "姓名",
                    prop: "name",
                    type: "input",
                    inputButtonShow: true,
                    inputButtonType: 'primary',
                    inputButtonText: '选择',
                    rules: [
                        { required: true, message: "请输入姓名", trigger: "blur" },
                        { min: 2, max: 10, message: "长度在 2 到 10 个字符", trigger: "blur" }
                    ]
                },
                {
                    label: "电话号码",
                    prop: "phone",
                    type: "input",
                    rules: [
                        { required: true, message: "请输入电话号码", trigger: "blur" },
                        {
                            validator: (rule, value, callback) => {
                                const phonereg = /^1[3-9]\d{9}$|^(\(\d{3,4\)|\d{3,4}-)?\d{7,8}$/;
                                if (phonereg.test(value)) {
                                    callback();
                                } else {
                                    callback(new Error('请输入正确的手机号码'));
                                }
                            }
                        }
                    ]
                },
                {
                    label: "年龄",
                    prop: ["ageMin", "ageMax"],
                    type: "rangeInput",
                    rules: [
                        { required: true, message: "请输入年龄", trigger: "blur" },
                        { type: "number", message: "请输入数字", trigger: "blur" },
                        { min: 1, max: 100, message: "年龄必须在 1 到 100 岁之间", trigger: "blur" }
                    ]
                },
                {
                    label: "地址",
                    prop: "address",
                    type: "input",
                    rules: [
                        { required: true, message: "请输入地址", trigger: "blur" },
                        { min: 5, max: 20, message: "长度在 5 到 20 个字符", trigger: "blur" }
                    ]
                },
                {
                    label: "性别",
                    prop: "gender",
                    type: "select",
                    options: [{ label: "男", value: 1 }, { label: "女", value: 2 }],
                    rules: [
                        { required: true, message: "请选择性别", trigger: "blur" }
                    ]
                },
                {
                    label: "生日",
                    prop: "birthday",
                    type: "date",
                    rules: [
                        { required: true, message: "请选择生日", trigger: "blur" }
                    ],
                },
                {
                    label: "在校时间",
                    prop: "schoolTime",
                    type: "dateRange",

                    rules: [
                        { required: true, message: "请选择生日", trigger: "blur" }
                    ],
                },
                {
                    label: "备注",
                    prop: "remark",
                    type: "textarea",
                }
            ],

4)没改样式的效果

CSS样式都没写,当前的效果如下:

每个item默认的宽度不一致,label长度不一致导致不齐,样式需要改进。

5)最终效果

经过一些修改之后,排版整齐了许多:

当屏幕缩小时,item长度也会随之变小:

当直接点击提交或者所在item未填写数据后失焦时,rules中的验证规则起到作用:

接下来验证数据,表单填写数据如下:

在开发者工具的控制台查看得到的数据,这里的输出的数据是父组件获取到的formData:

三、最终代码

最终代码如下:

1.父组件index.vue

复制代码
<template>
    <div class="container">
        <el-button @click="showFormPop">打开表单弹框</el-button>
    </div>
    <FormPop v-if="dialogFormVisible" :formItems="formItems" @closeCallBack="dialogFormVisible = false"
        @submitCallBack="handleConfirm">
    </FormPop>
</template>
  
<script>
import FormPop from "../../components/FormPop/index.vue";

export default {
    name: "index",
    components: {
        FormPop
    },
    data() {
        return {
            dialogFormVisible: false,
            formItems: [
                {
                    label: "姓名",
                    prop: "name",
                    type: "input",
                    inputButtonShow: true,
                    inputButtonType: 'primary',
                    inputButtonText: '选择',
                    rules: [
                        { required: true, message: "请输入姓名", trigger: "blur" },
                        { min: 2, max: 10, message: "长度在 2 到 10 个字符", trigger: "blur" }
                    ]
                },
                {
                    label: "电话号码",
                    prop: "phone",
                    type: "input",
                    rules: [
                        { required: true, message: "请输入电话号码", trigger: "blur" },
                        {
                            validator: (rule, value, callback) => {
                                const phonereg = /^1[3-9]\d{9}$|^(\(\d{3,4\)|\d{3,4}-)?\d{7,8}$/;
                                if (phonereg.test(value)) {
                                    callback();
                                } else {
                                    callback(new Error('请输入正确的手机号码'));
                                }
                            }
                        }
                    ]
                },
                {
                    label: "年龄",
                    prop: ["ageMin", "ageMax"],
                    type: "rangeInput"
                },
                {
                    label: "性别",
                    prop: "gender",
                    type: "select",
                    options: [{ label: "男", value: 1 }, { label: "女", value: 2 }],
                    rules: [
                        { required: true, message: "请选择性别", trigger: "blur" }
                    ]
                },
                {
                    label: "生日",
                    prop: "birthday",
                    type: "date",
                    rules: [
                        { required: true, message: "请选择生日", trigger: "blur" }
                    ],
                },
                {
                    label: "在校时间",
                    prop: "schoolTime",
                    type: "dateRange",

                    rules: [
                        { required: true, message: "请选择生日", trigger: "blur" }
                    ],
                },
                {
                    label: "备注",
                    prop: "remark",
                    type: "textarea",
                }
            ],
            formData: {
                name: "",
            }
        }
    },
    created() {
    },
    methods: {
        showFormPop() {
            console.log("弹窗显示")
            this.dialogFormVisible = true;
            console.log(this.dialogFormVisible)
        },
        handleCancel() {
            // 取消对话框弹窗
            this.dialogFormVisible = false;
            console.log("弹窗取消")
        },
        handleConfirm(formData) {
            // 处理弹窗提交
            this.dialogFormVisible = false;
            console.log("弹窗提交成功")
            console.log(formData)
        }
    }
};
</script>

<style scoped>
.container {
    display: flex;
    justify-content: center;
    align-items: center;
    margin-top: 20px;
}
</style>

2.子组件FormPop.vue

复制代码
<template>
    <el-dialog title="表单弹窗" v-model="dialogFormVisible" width="70%" :before-close="cancelClick">
        <div class="form-part">
            <el-form :model="formData" ref="form" class="form-content">
                <template v-for="(item, index) in formItems" :key="index">
                    <!-- 文本输入框、选择框、范围输入框 -->
                    <el-form-item v-if="['input', 'textarea', 'select', 'rangeInput'].includes(item.type)"
                        :label="item.label" :prop="item.prop" :rules="item.rules" label-width="100px" class="form-item">
                        <div v-if="item.type === 'input' || item.type === 'textarea'" class="input-part">
                            <el-input v-model="formData[item.prop]" :disabled="item.disabled || false" :clearable="true"
                                :type="item.type" :placeholder="item.disabled ? item.value : (item.placeholder || '请输入')" />
                            <template v-if="item.inputButtonShow">
                                <el-button plain :type="item.inputButtonType" @click="handleInputButtonClick(item, index)">
                                    {{ item.inputButtonText }}
                                </el-button>
                            </template>
                        </div>
                        <el-select v-if="item.type === 'select'" v-model="formData[item.prop]" :clearable="true"
                            :placecholder="item.placeholder || '请选择'">
                            <el-option v-for="(option) in item.options" :key="'item-' + option.value || option.id"
                                :label="option.label || option.name" :value="option.value || option.id" />
                        </el-select>
                        <template v-if="item.type === 'rangeInput'">
                            <div class="range-input">
                                <el-input-number :min="item.min || 1" :max="item.max || 100"
                                    v-model="formData[item.prop[0]]" style="width: 100%" :disabled="item.disabled || false"
                                    @change="handleRangeInputChange(item.prop[0], $event)">
                                </el-input-number>
                                <span class="dash">~</span>
                                <el-input-number :min="item.min || 1" :max="item.max || 100"
                                    v-model="formData[item.prop[1]]" style="width: 100%" :disabled="item.disabled || false"
                                    @change="handleRangeInputChange(item.prop[1], $event)">
                                </el-input-number>
                            </div>
                        </template>
                    </el-form-item>
                    <!-- 日期范围选择器 -->
                    <el-form-item v-if="['dateRange', 'date', 'monthRange'].includes(item.type)" :label="item.label"
                        :prop="item.prop" :rules="item.rules" label-width="100px" class="form-item">
                        <el-date-picker v-if="item.type === 'date'" v-model="formData[item.prop]" type="date"
                            :placeholder="item.placeholder || '请选择日期'" clearable style="width: 100%" format="YYYY-MM-DD"
                            value-format="YYYY-MM-DD" :disabled="item.disabled || false">
                        </el-date-picker>
                        <el-date-picker v-if="item.type === 'dateRange'" v-model="formData[item.prop]" type="daterange"
                            unlink-panels clearable range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期"
                            :disabled="item.disabled || false">
                        </el-date-picker>
                        <el-date-picker v-if="item.type === 'monthRange'" v-model="formData[item.prop]" type="monthrange"
                            unlink-panels clearable range-separator="-" start-placeholder="开始月份" end-placeholder="结束月份"
                            :disabled="item.disabled || false">
                        </el-date-picker>
                    </el-form-item>
                </template>
                <div class="footer">
                    <el-button type="primary" @click="submitForm">提交</el-button>
                    <el-button @click="resetForm">重置</el-button>
                </div>
            </el-form>
        </div>
    </el-dialog>
</template>
  
<script>

export default {
    name: "Index",
    data() {
        return {
            formData: {},
            formRules: {},
            dialogFormVisible: true,
        };
    },
    created() {
        console.log("created", this.formItems)
        console.log('age', this.formItems[2].prop[0])
    },
    props: {
        formItems: {
            type: Array,
            default: () => []
        }
    },
    mounted() {
        // 初始化formData中与formItems对应的字段值
        this.formItems.forEach((item) => {
            if (item.prop) {
                if (Array.isArray(item.prop)) {
                    // 对于rangeInput类型,prop是数组的情况
                    item.prop.forEach((prop, index) => {
                        this.formData[prop] = index === 0 ? item.min || 0 : item.max || 100;
                    });
                } else {
                    this.formData[item.prop] = '';
                }
            }
        });
    },
    methods: {
        cancelClick() {
            this.$emit("closeCallBack", false);
        },

        getFormData() {
            console.log("getFormData", this.formData)
            return new Promise((resolve, reject) => {
                this.$refs.form.validate((valid, fields) => {
                    if (valid) {
                        resolve();
                    } else {
                        reject(fields);
                    }
                });
            });
        },

        submitForm() {
            this.getFormData().then(() => {
                this.$emit("submitCallBack", this.formData);
            }).catch(() => {
                console.log("error");
            });
        },

        resetForm() {
            this.$refs.form.resetFields();
        },

        handleInputButtonClick(item, index) {
            console.log("handleInputButtonClick", item, index)
        },

        handleRangeInputChange(prop, event) {
            console.log('改变的值:', event, '数据类型:', typeof event);
            this.formData[prop] = event;
        }
    },
}
</script>
  
<style scoped>
@import '../../styles/FormPop.css';
</style>

3.样式文件FormPop.css

复制代码
* {
    margin: 0;
    padding: 0;
}

.form-item {
    width: 90%;
}

.input-part {
    width: 100%;
    display: flex;
}

.range-input {
    width: 100%;
    display: flex;
    flex-grow: 1;
}

.dash {
    margin: 0 5px;
}

.footer {
    display: flex;
    justify-content: center;
    margin-top: 20px;
    gap: 10%;
}

后续还可以在此基础上进一步扩展,比如增加radio、checkbox等选择框,tag标签等,以及可以结合状态管理器对form表单中的数据进行及时存储。

相关推荐
掘金者阿豪13 小时前
把业务数据变成共享仪表盘:Metabase可视化与远程访问实践
前端·后端
kyriewen14 小时前
折腾了半年 AI 编程工作流,最后发现效率瓶颈是桌上那块屏幕
前端·javascript·ai编程
蜗牛前端14 小时前
codex 全流程开发上线的高颜值礼簿小程序
前端·微信小程序
大龄秃头程序员15 小时前
我在图文流 App 里落地双层缓存、弱网降级与 OOM 治理
前端
老王以为15 小时前
React Renderer 分离的多平台架构
前端·react native·react.js
hunterandroid15 小时前
Kotlin Coroutines 与 Flow:让异步任务更清晰
前端
Bigger15 小时前
从零搭建 AI 代码审查服务:一份前端也能看懂的 Python 学习笔记
前端·ci/cd·ai编程
lichenyang45316 小时前
JSAPI、NAPI、Biz、Imp:ASCF Demo 如何真正调用系统能力和 C++ 能力
前端
lichenyang45316 小时前
IPC、JSVM、UIThread、libuv:ASCF 架构图里最容易混的几个词
前端
用户0595401744616 小时前
Redis记忆存储故障恢复测试踩坑实录:手动测试让我漏掉了2个一致性Bug
前端·css