配置化el-form的二次封裝之思路分析附上程式碼可直接使用

水冗水孚發表於2022-02-03

問題描述

個人愚見編寫程式碼其實就是:

  • 學習規則(看官方文件)
  • 使用規則(在使用的過程中進一步理解官方文件)
  • 最終基於原有底層官方文件規則再自定義新規則(封裝新的規則,便於複用)

所以本文講述一下基於原有的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>
好記性不如爛筆頭,記錄一下吧。歡迎批評指正 ^_^

相關文章