フロントエンド構成可能なフォームコンポーネントの設計方法 | JD Cloud 技術チーム

1. 背景

フロントエンド開発はフォーム関連のページが多く、機能がシンプルで開発が早いように見えますが、実はかなりの時間を割いています。フォームに含まれる要素が多すぎると、HTML コードが多すぎて、vue ファイルが大きすぎます。見つけ、変更し、維持することは容易ではありません。開発効率の向上と保守コストの削減を目的として、フォーム構成要素のカプセル化原理とカプセル化方法を以下に紹介します。

2. 技術的な解決策

上図のように、フォーム構成部品をカプセル化するポイントは3つあり、1つはフォーム要素の配置の問題をどう解決するか、もう1つはフォームデータのバインディング、3つ目はフォームのパラメータ構成検証です。フォーム要素。以下に、これら 3 つの問題に対する解決策を紹介します。

• 構成可能なフォーム コンポーネントの入力パラメータと説明

パラメータ 例証する タイプ オプション値 デフォルト
ラベル幅 フォーム要素のラベルが占める幅 —— 150px
列リスト フォーム要素で構成される構成は配列です 配列 —— []
フォームデータ フォーム要素値のコレクション 物体 —— {}
columnSpan フォーム配置列 番号 —— 24
サイズ フォーム要素のサイズ ミディアム / スモール / ミニ 中くらい

• 構成フォームの行数を計算する. このフォームは、フォームの最終的な行数と列数を基本の 24 列から計算し、最終的に次の方法で行と列の 2 次元配列を取得します。

newColumnList() {
  const newColumnList= []
  const row = Math.floor(24 / this.columnSpan)
  let newColumnItem = []
  for(let i=0; i< this.columnList.length; i++) {
    newColumnItem.push(this.columnList[i])
    if(newColumnItem.length === row || i === this.columnList.length-1) {
      newColumnList.push(newColumnItem)
      newColumnItem = []
    }
  }
  return newColumnList
}

• 上記で取得した 2 次元配列、最初のループ レンダリング行、および 2 番目のループ レンダリング列を介してレンダリングをループします。このソリューションは要素内のフォームを使用します。もちろん、他のコンポーネント ライブラリまたはネイティブ フォームでレンダリングすることもでき、原則は普遍的です。最終的に、どの特定のフォーム要素をロードするかは、パラメータ column.type に従って決定されます。

<el-form ref="form" :model="formData" :label-width="labelWidth" :size="size">
    <el-row :gutter="20" v-for="(element,index) in newColumnList" :key="index+'formRow'">
      <template v-for="(item, index) in element" >
        <column
          :key="index + 'formView'"
          :columnSpan="columnSpan"
          :column="item"
          :formData="formData"
        />
      </template>
    </el-row>
</el-form>

• 列コンポーネントは、タイプに応じて特定のフォーム要素を最終的にロードします。列コンポーネントの入力パラメーターと説明を以下に示します。さまざまなフォーム要素がコンポーネントを介して読み込まれます。

パラメータ 例証する タイプ オプション値 デフォルト
フォーム要素の特定の構成 物体 —— {}
フォームデータ フォーム要素値のコレクション 物体 —— {}
columnSpan フォーム配置列 番号 —— 24
<el-col :span="columnSpan">
     <component
      :is="column.type + 'View'"
      :column="column"
      :formData="formData"
      v-model="formData[column.name]"
      :columnSpan="columnSpan"/>
 </el-col>

• ここでは、フォーム要素の双方向バインディング、検証、および値の更新を説明するために、主に select form 要素を例として取り上げます。

パラメータ 例証する タイプ オプション値 デフォルト
フォーム要素の特定の構成 物体 —— {}
価値 フォーム要素の値 数値/文字列/配列 —— ——

•列パラメータ

パラメータ 例証する タイプ オプション値 デフォルト
プレースホルダ ヌル値の説明 —— ——
必要 必須ですか? ブール値 —— ——
ルール 検証規則 配列 —— ——
タイトル フォーム要素ラベル —— ——
名前 フォーム要素の値の名前 —— ——
多数 複数選択するかどうか ブール値 —— ——
ろ過可能 フィルタリングするかどうか ブール値 —— ——
無効 無効にするかどうか ブール値 —— ——
辞書 ドロップダウン オプションの列挙 配列 —— ——
変更機能 値が変化したときのコールバック関数 関数 —— ——
<el-form-item :label="column.title + ':'" :prop="column.name" :rules="rules">
    <el-select
      v-model="val"
      clearable
      :multiple="column.multiple"
      :filterable="column.filterable"
      :placeholder="'请选择' + column.title"
      :disabled="column.disabled"
      style="width: 100%"
      @change="onChange"
      @clear="onClear">
      <el-option v-for="item in column.dictionary" :key="item.code" :label="item.name" :value="item.code">
      </el-option>
    </el-select>
</el-form-item>
rules:  [
    {
      required: this.column.required,
      message: this.column.placeholder placeholder ? this.column.placeholder : `请输入${this.column.title}`,
      trigger: 'change'
    },
    ...this.column.rules
 ]
onChange(){
  this.$emit('input',this.val)
  if(this.column && this.column.changeFunction){
    this.column.changeFunction(this.val)
  }
},
onClear(){
  this.onChange()
}

3. プロジェクトの実践

• 構成フォームは bs-form であり、bs-form フォーム コンポーネントがページに導入されています。

<bs-form ref="formDemo"
     :columnList="columnList"
     :formData="formData"
     :columnSpan="columnSpan"
     labelWidth="120px">
</bs-form>
<el-row style="text-align: center;">
  <el-button type="primary"
             @click="onSave">保存</el-button>
  <el-button @click="onCancel">取消</el-button>
</el-row>

• formData パラメータ

formData: {
    name: '',
    yearIncome: '', // 业务类型
    goodsCategoryId: '', // 托寄物品类id
    projectManagerErp: '', // 项目经理erp
    projectName: '', // 项目名称
    projectStage: '', // 项目阶段编码
    projectStandardName: '', // 标准名称
    projectYear: 2023, // 年份
    startRegionId: '', // 始发区域id
    startBattleId: '', // 始发战区id
    address: [], // 省市
    category: null, //图文类型
    range: [] //发布范围
 }

• 列パラメータ

columnSpan: 6

• フォーム構成パラメータ

columnList(){
  const self = this
  return [
    {
      type: 'text',
      name: 'name',
      title: '项目名称',
      required: true,
      maxlength: 20,
      showwordlimit: true,
      placeholder: '请输入'
    },
    {
      name: 'category',
      type: 'radio',
      dictionary: [
        {
          code: 1,
          name: '类型一'
        },
        {
          code: 2,
          name: '类型二'
        }
      ],
      title: '图文类型',
      required: true
    },
    {
      name: 'range',
      type: 'checkbox',
      title: '发布范围',
      dictionary: [
        {
          code: 1,
          name: '范围一'
        },
        {
          code: 2,
          name: '范围二'
        }
      ],
      required: true
    },
    {
      type: 'text',  // 字段类型文本框
      name: 'yearIncome',  //与后台对接字段
      title: '年均收入',  // 前端展示字段
      required: true, // 必填项设置
      maxlength: 50,  // 字符串长度限制
      showwordlimit: true, // 是否显示字符串长度
      placeholder: '请输入', // 占位文本提示
      rules: [
        { pattern: /(^[1-9]([0-9]+)?(.[0-9]{1,2})?$)|(^(0){1}$)|(^[0-9].[0-9]([0-9])?$)/, message: '请输入数字最多两位小数' }
      ],
    },
    {
      type: 'select',
      name: 'goodsCategoryId',
      title: '托寄物品类',
      required: true,
      filterable: true,
      placeholder: '请选择',
      dictionary: [{
        name: '苹果',
        code: '1'
      },{
        name: '手机',
        code: '2'
      },{
        name: '测试',
        code: '3'
      },{
        name: '樱桃',
        code: '7'
      },{
        name: '荸荠',
        code: '9'
      }]
    },
    {
      type: 'select',
      name: 'startRegionId',
      title: '区域',
      required: true,
      placeholder: '请选择',
      dictionary: [{
        name: '销售-华北区域',
        code: '1'
      },{
        name: '销售-华东区域',
        code: '2'
      },{
        name: '销售-华南区域',
        code: '3'
      },{
        name: '销售-西南区域',
        code: '4'
      },{
        name: '销售-华中区域',
        code: '5'
      },{
        name: '销售-东北区域',
        code: '6'
      }],
      // 点击下来触发切换联动的事件,为一个函数
      changeFunction: function (val) {
      }
    }, {
      type: 'select',
      name: 'startBattleId',
      title: '战区',
      required: true,
      placeholder: '请选择',
      dictionary: this.battleByRegionList
    }, {
      type: 'select',
      name: 'projectStage',
      title: '项目阶段',
      required: true,
      placeholder: '请选择',
      dictionary: [{
        name: '项目发起阶段',
        code: '10'
      },{
        name: '项目调研阶段',
        code: '20'
      },{
        name: '可行性分析阶段',
        code: '30'
      },{
        name: '立项阶段',
        code: '40'
      }]
    }, {
      type: 'text',
      name: 'projectStandardName',
      title: '标准名称',
      required: true,
      placeholder: '请输入',
      append: '.com',  // 文本框后置内容
    }, {
      type: 'text',
      name: 'projectManagerErp',
      title: '项目经理',
      required: true,
      placeholder: '请输入'
    },{
      type: 'cascader',  // 字段类型下拉框
      name: 'address',   //与后台对接字段
      title: '省市区',  // 前端展示字段
      required: true, // 必填项设置
      placeholder:'请选择',  // 占位文本提示
      dictionary: [{
        value: 'shanxi',
        label: '陕西省',
        children: [{
          value: 'xian',
          label: '西安市',
          children: [{
            value: 'yanta',
            label: '雁塔区'
          }, {
            value: 'beilin',
            label: '碑林区'
          }, {
            value: 'xincheng',
            label: '新城区'
          }, {
            value: 'weiyang',
            label: '未央区'
          }]
        }]
      }],
      // 点击下来触发切换联动的事件,为一个函数
      changeFunction: function(){}
    },{
      type: 'static',
      name: 'projectYear',
      title: '年份'
    }
  ]
}

• フォームの保存

// 保存
async onSave() {
  const valid = await this.$refs.formDemo.onValidate()
  if(valid) {
    this.$message.success('校验通过')
  }else {
    this.$message.error('校验失败')
  }
}

4. 結果表示

著者: JD Logistics Tian Leilei

{{o.name}}
{{m.name}}

おすすめ

転載: my.oschina.net/u/4090830/blog/8704947