theme: cyanosis
highlight: a11y-dark
问题描述
个人愚见编写代码其实就是:
- 学习规则(看官方文档)
- 使用规则(在使用的过程中进一步理解官方文档)
- 最终基于原有底层官方文档规则再自定义新规则(封装新的规则,便于复用)
所以本文讲述一下基于原有的el-form的规则,进行二次封装自定义新的规则的思路,以及附上能直接用的代码。我们先看一下效果图:
效果图
思路分析
最终效果是配置化“写代码”,就像echarts一样,写不同的配置,出现不同的效果,自然是配置,所以就要提前考虑好有哪些需要配置。当然也要考虑数据的回显。
- 配置表单项类型(组件中要加上校验规则)
- 配置表单项的名字
- 配置表单项的字段
- 配置表单项是否必填
- 配置输入框的单位(如果有的话)
- 配置placeholder的文本提示
- 配置下拉框选项数据数据(如果是固定的下拉框可以传过去)
- 如果是枚举值类型的下拉框就需要发请求获取下拉框选项数组数据
- 等...
这里要多提一下表单项类型
配置表单项~输入框的类型
首先我们要清楚form表单项的类型,这里为了便于理解,只举例三种大类型,当然大类型中也包含小类型,同时也要做校验。至于别的类型,大家理解了这几个类型以后,就可以自己写了。
输入框类型
- 文本输入框类型(校验得填写,不能为空)
- 数字输入框类型(校验输入的数字类型,比如需要正整数、需要保留两位小数等)
下拉框类型
- 固定选项的下拉框类型(这里直接写死,传过去即可,比如性别下拉框,只有男女两种类型选项)
- 枚举多个选项的单选下拉框类型(需要提前发请求获取数据,或者visible-change事件发请求获取)
- 枚举多个选项的单选多选下拉框类型(同上)
时间选择器范围类型
- 注意绑定的结果值是数组即可
最后不要忘了回显逻辑哦
el-form表头数据举例
子组件表单数据根据根据父组件传递过来的formHeader动态渲染。即v-for中搭配v-if去呈现,先简单看一下formHeader数据结构,具体在后边代码中都有的
// 表头数组数据
formHeader: [
{
itemType: "text", // 输入框类型
labelName: "姓名", // 输入框名字
propName: "name", // 输入框字段名
isRequired: true, // 是否必填
placeholder: "请填写名字", // 输入框placeholder提示语加上,可用于告知用户规则
},
{
itemType: "number",
labelName: "年龄",
propName: "age",
isRequired: true,
unit: "year", // 数字类型的要有单位
placeholder: "请输入年龄(大于0的正整数)",
},
{
itemType: "selectOne", // 下拉框类型一,固定的选项可以写死在配置里,比如性别只有男女
labelName: "性别",
propName: "gender",
isRequired: true,
placeholder: "请选择性别",
optionsArr: [
{
label: "男",
value: 1,
},
{
label: "女",
value: 2,
},
],
},
],
完整代码
建议复制粘贴,运行跑起来,这样效果更加明显,更便于理解。
毕竟:no words,show codes
父组件传递配置数据
<template>
<div class="myWrap">
<h2>填写表单</h2>
<br />
<my-form
ref="myForm"
:formHeader="formHeader"
@submitForm="submitForm"
@resetForm="resetForm"
></my-form>
<h2>表单数据回显</h2>
<el-button size="small" type="primary" @click="showData"
>点击按钮回显数据</el-button
>
</div>
</template>
<script>
import myForm from "./myForm.vue";
export default {
components: {
myForm,
},
data() {
return {
// 表头数组数据
formHeader: [
/**
* 输入框类型3种
* 1. 普通文本输入框 text
* 2. 数字类型输入框 number
* 3. 文本域输入框 textarea
*
* 下拉框select类型2中
* 1. 固定配置的el-option selectOne
* 2. 枚举值的el-option单选 selectTwo
* 2. 枚举值的el-option多选 selectThree
*
* 时间选择器类型1种
* 1. 两个时间选择器、选取一个范围
*
* 等等,还有其他类型,这里举三种类型,别的类型仿照着即可写出来
* 组件封装适可而止。如果是比较复杂(奇葩)的需要联动的表单,建议一个个写
* 毕竟过度的封装,会导致代码不好维护(个人愚见)
*
* */
{
itemType: "text", // 输入框类型
labelName: "姓名", // 输入框名字
propName: "name", // 输入框字段名
isRequired: true, // 是否必填
placeholder: "请填写名字", // 输入框placeholder提示语加上,可用于告知用户规则
},
{
itemType: "number",
labelName: "年龄",
propName: "age",
isRequired: true,
unit: "year", // 数字类型的要有单位
placeholder: "请输入年龄(大于0的正整数)",
},
{
itemType: "number",
labelName: "工资",
propName: "salary",
isRequired: true,
unit: "元/月", // 数字类型的要有单位
placeholder: "请输入每月工资金额(大于0且保留两位小数)",
},
{
itemType: "textarea",
labelName: "备注",
propName: "remark",
isRequired: true,
placeholder: "请填写备注",
},
{
itemType: "selectOne", // 下拉框类型一,固定的选项可以写死在配置里,比如性别只有男女
labelName: "性别",
propName: "gender",
isRequired: true,
placeholder: "请选择性别",
optionsArr: [
{
label: "男",
value: 1,
},
{
label: "女",
value: 2,
},
],
},
{
itemType: "selectTwo", // 下拉框类型二,枚举值单选,在点击下拉选项时根据枚举id发请求,获取枚举值
labelName: "可选职业",
propName: "job",
isRequired: true,
placeholder: "请选择职业",
enumerationId: "123123123",
},
{
itemType: "selectTwo", // 下拉框类型二,枚举值单选,在点击下拉选项时根据枚举id发请求,获取枚举值
labelName: "愿望",
propName: "wish",
isRequired: true,
placeholder: "请选择愿望",
enumerationId: "456456456",
},
{
itemType: "selectThree", // 下拉框类型三,枚举值多选,在点击下拉选项时根据枚举id发请求,获取枚举值
labelName: "爱好",
propName: "hobby",
isRequired: true,
placeholder: "请选择爱好",
enumerationId: "789789789",
},
{
itemType: "selectThree", // 下拉框类型三,枚举值多选,在点击下拉选项时根据枚举id发请求,获取枚举值
labelName: "想买手机",
propName: "wantPhone",
isRequired: true,
placeholder: "请选择手机",
enumerationId: "147258369",
},
{
itemType: "dateRange", // 日期范围类型
labelName: "日期",
propName: "date",
isRequired: true,
},
],
};
},
mounted() {
// 数据回显的时候,要先发请求获取枚举值下拉框的值才能够正确的回显,所以
// 就提前发请求获取对应下拉框的值了,这里要注意!注意!注意!
this.formHeader.forEach((item) => {
if ((item.itemType == "selectTwo") | (item.itemType == "selectThree")) {
this.$refs.myForm.getOptionsArrData(item);
}
});
},
methods: {
showData() {
let apiData = {
name: "孙悟空",
age: 500,
salary: 6666.66,
remark: "齐天大圣是也",
gender: 1, // 1代表男
job: 1, // 1医生 2教师 3公务员
wish: 3, // 1成为百万富翁 2长生不老 3家人健康幸福平安
hobby: [1, 2, 3], // 1乒乓球 2羽毛球 3篮球
wantPhone: [1, 2, 4], // 1华为 2小米 3苹果 4三星
date: ["2018-06-06", "2022-05-05"],
};
setTimeout(() => {
this.$refs.myForm.form = apiData;
}, 300);
},
submitForm(form) {
console.log("表单提交喽", form);
},
resetForm() {
console.log("表单重置喽");
},
},
};
</script>
<style lang='less' scoped>
.myWrap {
width: 100%;
height: 100%;
box-sizing: border-box;
padding: 25px;
overflow-y: auto;
}
</style>
封装的子组件根据传递的配置数据动态渲染
<template>
<div class="formWrap">
<el-form ref="form" label-position="top" :model="form" label-width="80px">
<template v-for="(item, index) in formHeader">
<!-- 当类型为普通文本输入框时 -->
<el-form-item
v-if="item.itemType == 'text'"
:key="index"
:label="item.labelName"
:prop="item.propName"
:rules="
item.isRequired
? [
{
required: true, // 是否必填 是
trigger: 'blur', // 触发方式,失去焦点
itemType: 'text', // 当前类型,文字输入框
labelName: item.labelName, // 当前输入框的名字
value: form[item.propName], // 输入框输入的绑定的值
validator: validateEveryData, // 校验规则函数
},
]
: []
"
>
<el-input
:placeholder="item.placeholder"
v-model.trim="form[item.propName]"
clearable
size="small"
></el-input>
</el-form-item>
<!-- 当类型为数字类型输入框时 -->
<el-form-item
v-if="item.itemType == 'number'"
:key="index"
:label="item.labelName"
:prop="item.propName"
:rules="
item.isRequired
? [
{
required: true, // 是否必填 是
trigger: 'blur', // 触发方式,失去焦点
itemType: 'number', // 当前类型,文字输入框
labelName: item.labelName, // 当前输入框的名字
value: form[item.propName], // 输入框输入的绑定的值
validator: validateEveryData, // 校验规则函数
},
]
: []
"
>
<el-input
:placeholder="item.placeholder"
v-model.trim="form[item.propName]"
@change="checkInput(item)"
clearable
size="small"
>
<span slot="suffix">{{ item.unit }}</span>
</el-input>
</el-form-item>
<!-- 当类型为文本域输入框时 -->
<el-form-item
v-if="item.itemType == 'textarea'"
:key="index"
:label="item.labelName"
:prop="item.propName"
:rules="
item.isRequired
? [
{
required: true, // 是否必填 是
trigger: 'blur', // 触发方式,失去焦点
itemType: 'textarea', // 当前类型,文本域输入框
labelName: item.labelName, // 当前输入框的名字
value: form[item.propName], // 输入框输入的绑定的值
validator: validateEveryData, // 校验规则函数
},
]
: []
"
>
<el-input
type="textarea"
:placeholder="item.placeholder"
v-model.trim="form[item.propName]"
clearable
size="small"
></el-input>
</el-form-item>
<!-- 当类型为下拉框一时,固定下拉选项 -->
<el-form-item
v-if="item.itemType == 'selectOne'"
:key="index"
:label="item.labelName"
:prop="item.propName"
:rules="
item.isRequired
? [
{
required: true, // 是否必填 是
trigger: '', // blur 或 change 这里就不指定触发方式了,保存提交时再校验
itemType: 'selectOne', // 当前类型,固定下拉框类型
labelName: item.labelName, // 当前输入框的名字
value: form[item.propName], // 输入框输入的绑定的值
validator: validateEveryData, // 校验规则函数
},
]
: []
"
>
<el-select
v-model="form[item.propName]"
:placeholder="item.placeholder"
clearable
size="small"
>
<el-option
v-for="(ite, ind) in item.optionsArr"
:key="ind"
:label="ite.label"
:value="ite.value"
></el-option>
</el-select>
</el-form-item>
<!-- 当类型为下拉框二时,属于枚举值(单选)下拉框,需要根据枚举id发请求获取枚举值 -->
<el-form-item
v-if="item.itemType == 'selectTwo'"
:key="index"
:label="item.labelName"
:prop="item.propName"
:rules="
item.isRequired
? [
{
required: true, // 是否必填 是
trigger: '', // blur 或 change 这里就不指定触发方式了,保存提交时再校验
itemType: 'selectTwo', // 当前类型,枚举值单选
labelName: item.labelName, // 当前输入框的名字
value: form[item.propName], // 输入框输入的绑定的值
validator: validateEveryData, // 校验规则函数
},
]
: []
"
>
<el-select
v-model="form[item.propName]"
:placeholder="item.placeholder"
clearable
@visible-change="
(flag) => {
getOptionsArr(flag, item);
}
"
:loading="loadingSelect"
size="small"
>
<el-option
v-for="(ite, ind) in selectTwoOptionsObj[item.propName]"
:key="ind"
:label="ite.label"
:value="ite.value"
></el-option>
</el-select>
</el-form-item>
<!-- 当类型为下拉框三时,属于枚举值(多选)下拉框,需要根据枚举id发请求获取枚举值 -->
<el-form-item
v-if="item.itemType == 'selectThree'"
:key="index"
:label="item.labelName"
:prop="item.propName"
:rules="
item.isRequired
? [
{
required: true, // 是否必填 是
trigger: 'blur', // 这里用blur,防止初次默认校验触发
itemType: 'selectThree', // 当前类型,枚举值多选
labelName: item.labelName, // 当前输入框的名字
value: form[item.propName], // 输入框输入的绑定的值
validator: validateEveryData, // 校验规则函数
type: 'number',
},
]
: []
"
>
<el-select
v-model="form[item.propName]"
:placeholder="item.placeholder"
clearable
@visible-change="
(flag) => {
getOptionsArr(flag, item);
}
"
:loading="loadingSelect"
multiple
collapse-tags
size="small"
>
<el-option
v-for="(ite, ind) in selectTwoOptionsObj[item.propName]"
:key="ind"
:label="ite.label"
:value="ite.value"
></el-option>
</el-select>
</el-form-item>
<!-- 当类型为日期范围 -->
<el-form-item
v-if="item.itemType == 'dateRange'"
:key="index"
:label="item.labelName"
:prop="item.propName"
:rules="
item.isRequired
? [
{
required: true, // 是否必填 是
trigger: '',
itemType: 'dateRange', // 当前类型,枚举值多选
labelName: item.labelName, // 当前输入框的名字
value: form[item.propName], // 输入框输入的绑定的值
validator: validateEveryData, // 校验规则函数
},
]
: []
"
>
<el-date-picker
v-model="form[item.propName]"
format="yyyy-MM-dd"
value-format="yyyy-MM-dd"
clearable
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
size="small"
>
</el-date-picker>
</el-form-item>
</template>
</el-form>
<!-- 提交表单和重置表单部分 -->
<div class="btns">
<el-button type="primary" @click="submitForm" size="small"
>保存</el-button
>
<el-button @click="resetForm" size="small">重置</el-button>
</div>
</div>
</template>
<script>
export default {
props: {
// 父组件传递过来的表头的数据
formHeader: {
type: Array,
default: () => {
return [];
},
},
},
data() {
var validateEveryData = (rule, value, callback) => {
// console.log("callback", callback);
// console.log("校验某一项的规则对象", rule);
// console.log("用户输入的值", value);
// 对输入框类型的校验
if (value) {
if ((value + "").length > 0) {
// 用于回显时候的校验,因为输入的时候是字符串类型的数字,但是回显的时候可能就是数字
callback(); // cb函数告知校验结果,必须要加
return;
}
}
// 对下拉框类型的校验
if (
(rule.itemType == "selectOne") |
(rule.itemType == "selectTwo") |
(rule.itemType == "selectThree")
) {
if (value) {
if ((value + "").length > 0) {
// 注意枚举值是数字类型的,所以这里要转换成为字符串类型的
callback();
return;
}
}
}
// 根据不同的类型给予不同的校验提示
switch (rule.itemType) {
case "text":
callback(new Error(rule.labelName + "不能为空")); // 文本类型的规则简单,就是得填写
break;
case "number":
callback(new Error(rule.labelName + "请按规则填写")); // 数字类型的规则比较繁多
break;
case "textarea":
callback(new Error(rule.labelName + "不能为空")); // 文本域类型的规则也简单,就是得填写
break;
case "selectOne":
callback(new Error("请选择" + rule.labelName)); // 下拉框类型一 得填写
break;
case "selectTwo":
callback(new Error("请选择" + rule.labelName)); // 下拉框类型二 得填写
break;
case "selectThree":
callback(new Error("请选择" + rule.labelName)); // 下拉框类型三 多选数组得填写
break;
case "dateRange":
callback(new Error("请选择" + rule.labelName + "范围")); // 下拉框类型三 多选数组得填写
break;
default:
break;
}
};
return {
// 此对象用于存储各个下拉框的数组数据值,其实也可以挂在vue的原型上,不过个人认为写在data中好些
selectTwoOptionsObj: {},
// 用于下拉框加载时的效果
loadingSelect: false,
// 绑定的数据
form: {},
// 校验规则
validateEveryData: validateEveryData,
};
},
methods: {
// 获取下拉框数据
async getOptionsArr(flag, item) {
// console.log(flag, item);
// 为true时表示展开,这里模拟根据枚举值id发请求,获取下拉框的值的
if (flag) {
this.loadingSelect = true; // 使用了加载中效果,最好加上一个try catch捕获异常
// let result = await this.$api.getEnumList({id:item.enumerationId})
this.getOptionsArrData(item);
} else {
// 解决多选下拉框失去焦点校验规则仍然存在问题
if (item.itemType == "selectThree") {
// console.log("关闭时校验多选值", this.form[item.propName]);
if (this.form[item.propName].length > 0) {
// 如果至少选择一个了,说明符合要求,就再校验一次,这样校验规则就去掉了
this.$refs.form.validateField(item.propName);
}
}
}
},
getOptionsArrData(item) {
setTimeout(() => {
this.loadingSelect = false;
if (item.enumerationId == "123123123") {
this.selectTwoOptionsObj[item.propName] = [
{
label: "医生",
value: 1,
},
{
label: "教师",
value: 2,
},
{
label: "公务员",
value: 3,
},
];
}
if (item.enumerationId == "456456456") {
this.selectTwoOptionsObj[item.propName] = [
{
label: "成为百万富翁",
value: 1,
},
{
label: "长生不老",
value: 2,
},
{
label: "家人健康幸福平安",
value: 3,
},
];
}
if (item.enumerationId == "789789789") {
this.selectTwoOptionsObj[item.propName] = [
{
label: "乒乓球",
value: 1,
},
{
label: "羽毛球",
value: 2,
},
{
label: "篮球",
value: 3,
},
];
}
if (item.enumerationId == "147258369") {
this.selectTwoOptionsObj[item.propName] = [
{
label: "华为",
value: 1,
},
{
label: "小米",
value: 2,
},
{
label: "苹果",
value: 3,
},
{
label: "三星",
value: 4,
},
];
}
this.$forceUpdate(); // 这里需要强制更新一下,否则渲染不出来下拉框选项
}, 300);
},
// 数字类型加校验规则
checkInput(item) {
console.log("数字类型的再细分规则,可以根据item.labelName再写判断", item);
if (item.labelName == "年龄") {
let reg = /^[1-9]\d*$/;
if (reg.test(this.form[item.propName] * 1)) {
// console.log("符合要求,年龄大于0的正整数");
} else {
this.form[item.propName] = null;
}
}
if (item.labelName == "工资") {
let reg = /^((0{1}\.\d{1,2})|([1-9]\d*\.{1}\d{1,2})|([1-9]+\d*))$/;
if (reg.test(this.form[item.propName] * 1)) {
// console.log("符合要求,工资保留两位小数");
this.form[item.propName] = (this.form[item.propName] * 1).toFixed(2);
} else {
this.form[item.propName] = null;
}
}
if ("某个数字类型字段值") {
// 加对应规则
}
},
// 保存提交表单
async submitForm() {
this.$refs.form.validate((valid) => {
if (valid) {
this.$emit("submitForm", this.form);
} else {
console.log("error submit!!");
return false;
}
});
},
// 重置表单
resetForm() {
this.$refs.form.resetFields();
this.form = {}; // 这里重置完了以后,要重新初始化数据,否则会出现输入不上去的问题
this.$emit("resetForm");
},
},
};
</script>
<style lang='less' scoped>
.formWrap {
width: 100%;
/deep/ .el-form {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
.el-form-item {
width: 47%;
margin-bottom: 12px !important;
.el-form-item__label {
padding: 0 !important;
line-height: 24px !important;
}
.el-form-item__content {
// 给文本域类型定高度
.el-textarea {
textarea {
height: 75px !important;
}
}
// 给下拉框指定宽度百分比
.el-select {
width: 100% !important;
}
// 时间选择器指定宽度百分比
.el-date-editor {
width: 100% !important;
.el-range-separator {
width: 10% !important;
}
}
.el-form-item__error {
padding-top: 1px !important;
}
}
}
}
.btns {
width: 100%;
text-align: center;
margin-top: 12px;
}
}
</style>
好记性不如烂笔头,记录一下吧。欢迎批评指正
^_^