問題描述
個人愚見編寫程式碼其實就是:
- 學習規則(看官方文件)
- 使用規則(在使用的過程中進一步理解官方文件)
- 最終基於原有底層官方文件規則再自定義新規則(封裝新的規則,便於複用)
所以本文講述一下基於原有的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>
好記性不如爛筆頭,記錄一下吧。歡迎批評指正 ^_^