目录
一、基本框架
1.父组件index.vue
2.子组件FormPop.vue
二、细节补充
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表单中的数据进行及时存储。