vue动态表单简单设计template版本

172 阅读3分钟

前言

前端页面存在大部分的条件搜索的需求,相似度高,复用性强。

基于vue3和element实现,当然内部的组件不是重点可以随意切换

基于JSON对象自动生成,后期也可探索加入可视化拖拽界面

基础页面

  interface queryFormConfig {
    prop: string,
    type: string,
    label?: string | undefined,
    slotName?: string | undefined,
    options?: Array<options>,
    rules?: Array<object>,
    children?: Array<object>
    dictCode?: string,
    showLabel?: boolean,
    unit?: string,
  }

  interface options {
    value: string,
    label: string
  }
}

// 配置对象案例
动态生成页面
{ prop: "a", type: "input", label: "输入框一号:" },
{ prop: "b", type: "cascader", dictCode: '2', label: "输入框二号:" },
{
    prop: "c",
    dictCode: 'ca', // 字典码,下拉框点击的时候自动获取字典数据
    type: "multipleSelect",
    label: "输入框三号:",
},
{
    prop: "21",
    dictCode: '32',
    type: "multipleSelect",
    label: "输入框四号:",
},
{ prop: "2", type: "picker", label: "输入框五号:" },
{ prop: "232", type: "cascader", label: "输入框六号:", },
{ prop: "32", type: "picker", label: "输入框七号:" },
{
    prop: "43",
    type: "complexSelector",
    label: "复合输入框八号:",
    children: [
      {
        label: "输入框八号:",
        prop: "fs",
        type: "amountRange",
      },
      {
        label: "输入框八号多选框:",
        prop: "dsa",
        type: "checkbox",
        options: [
          { label: "dsa", prop: "1" },
          { label: "dsa", prop: "2" },
          { label: "dsa", prop: "3" },
        ],
      },
      {
        label: "输入框八号单选框:",
        prop: "dads",
        type: "radio",
        options: [
          { label: "sa", prop: "1" },
          { label: "dsa", prop: "2" }
        ],
        checkbox: 'dada'
      }
    ],
  },
<el-form
      :model="transfer"
      :disabled="disabled"
      label-width="120px"
      ref="formRef"
      :rules="rules"
    >
      <el-row>
        <el-col
          v-for="(item, index) of queryFormConfig"
          :span="elColSpan(item.type || '')"
          :key="`col_${item.prop}_${item.type}_${index}`"
        >
          <el-form-item
            v-if="item.prop"
            :key="`item_${item.prop}_${item.type}_${index}`"
            :label="titleDisplay(item.type, item.label)"
            :label-width="labelWidth(item.type)"
          >
            <template v-if="item.slotName != null && item.slotName != ''">
              <slot
                :key="`slot_${item.prop}_${item.type}_${index}`"
                :name="item.slotName"
                :item="item"
                :index="index"
              ></slot>
            </template>

            <template v-if="item.type == 'interval'">
              <div style="display: flex">
                <el-input
                  :key="`item_interval_start_${item.prop}_${item.type}_${index}`"
                  style="width: 45%"
                  oninput="value = value.replace(/[^0-9]/g,'')"
                  v-model.number="transfer[`${item.prop}_start`]"
                  :placeholder="`${item.placeholder || '请输入'} `"
                  :clearable="`${item.clearable || true}`"
                >
                  <template #append v-if="item.unit"
                    >{{
                      ["", "㎡", "m", "层", "km/h", "个", "床", "吨", "%", "岁"][
                      item.unit
                    ]
                    }}
                  </template>
                </el-input>

                <span style="margin: 0 10px"> - </span>

                <el-input
                  :key="`item_interval_end_${item.prop}_${item.type}_${index}`"
                  style="width: 45%"
                  oninput="value = value.replace(/[^0-9]/g,'')"
                  v-model.number="transfer[`${item.prop}_end`]"
                  :placeholder="`${item.placeholder || '请输入'} `"
                  :clearable="`${item.clearable || true}`"
                >
                  <template #append v-if="item.unit">{{
                    ["", "㎡", "m", "层", "km/h", "个", "床", "吨", "%", "岁"][
                      item.unit
                    ]
                  }}</template>
                </el-input>
              </div>
            </template>

            <template v-if="!item.type || item.type == 'input'">
              <el-input
                :key="`input_${item.prop}_${item.type}_${index}`"
                v-model="transfer[`${item.prop}`]"
                :placeholder="`${item.placeholder || '请输入'} `"
                :clearable="`${item.clearable || true}`"
              />
            </template>

            <template v-if="item.type == 'numberinput'">
              <el-input
                oninput="value = value.replace(/[^0-9]/g,'')"
                :key="`numberinput_${item.prop}_${item.type}_${index}`"
                v-model.number="transfer[`${item.prop}`]"
                :placeholder="`${item.placeholder || '请输入'} `"
                :clearable="`${item.clearable || true}`"
              />
            </template>

            <template v-if="item.type == 'picker'">
              <el-date-picker
                v-model="transfer[`${item.prop}`]"
                type="daterange"
                :prefix-icon="Calendar"
                value-format="YYYY-MM-DD"
                range-separator="-"
                start-placeholder="开始日期"
                end-placeholder="结束日期"
              />
            </template>

            <template v-if="item.type == 'select'">
              <template
                v-if="Array.isArray(item.options) && item.options.length > 0"
              >
                <el-select
                  :key="`select_${item.prop}_${item.type}_${index}`"
                  v-model="transfer[`${item.prop}`]"
                  class="m-2"
                  placeholder="请选择"
                  collapse-tags
                  :loading="dictionaryLoading"
                  collapse-tags-tooltip
                >
                  <el-option
                    v-for="(e, index1) in item.options"
                    :key="`selectOption_${index1}`"
                    :label="e.label"
                    :value="e.value"
                  />
                </el-select>
              </template>
              <template v-else>
                <el-select
                  :key="`select_${item.prop}_${item.type}_${index}`"
                  v-model="transfer[`${item.prop}`]"
                  class="m-2"
                  placeholder="请选择"
                  collapse-tags
                  :loading="dictionaryLoading"
                  collapse-tags-tooltip
                  @focus="selectChange(item.dictCode, item.prop)"
                >
                  <el-option
                    v-for="e in store.selectOptions[item.prop]"
                    :key="e.id"
                    :label="e.itemText"
                    :value="e.itemValue"
                  />
                </el-select>
              </template>
            </template>

            <template v-if="item.type == 'labelSelector'"> </template>

            </template>

            <template v-if="item.type == 'multipleSelect'">
              <template
                v-if="Array.isArray(item.options) && item.options.length > 0"
              >
                <el-select
                  :key="`multipleSelec_${item.prop}_${item.type}_${index}`"
                  v-model="transfer[`${item.prop}`]"
                  class="m-2"
                  placeholder="请选择"
                  multiple
                  collapse-tags
                  :loading="dictionaryLoading"
                  collapse-tags-tooltip
                >
                  <el-option
                    v-for="(e, index1) in item.options"
                    :key="`selectOption_${index1}`"
                    :label="e.label"
                    :value="e.prop"
                  />
                </el-select>
              </template>
              <template v-else>
                <el-select
                  :key="`multipleSelec_${item.prop}_${item.type}_${index}`"
                  v-model="transfer[`${item.prop}`]"
                  class="m-2"
                  placeholder="请选择"
                  multiple
                  collapse-tags
                  :loading="dictionaryLoading"
                  collapse-tags-tooltip
                  @focus="selectChange(item.dictCode, item.prop)"
                >
                  <el-option
                    v-for="e in store.selectOptions[item.prop]"
                    :key="e.id"
                    :label="e.itemText"
                    :value="e.itemValue"
                  />
                </el-select>
              </template>
            </template>

            <template v-if="item.type == 'cascader'">
              <el-cascader
                v-model="transfer[`${item.prop}`]"
                @focus="cascaderChange(item.dictCode, item.prop)"
                :options="store.selectOptions[item.prop]"
                placement="bottom-end"
                :props="
                  item.cascaderProps || { multiple: true, checkStrictly: false }
                "
                collapse-tags
                clearable
              />
            </template>

            <template v-if="item.type == 'remoteSelect'">
              <el-select
                v-model="transfer[`${item.prop}`]"
                multiple
                filterable
                remote
                :reserve-keyword="false"
                placeholder="请输入"
                remote-show-suffix
                :remote-method="remoteMethod"
                :loading="loading"
                style="width: 240px"
              >
                <el-option
                  v-for="item in projectNameOptions"
                  :key="item.id"
                  :label="item.name"
                  :value="item.id"
                />
              </el-select>
            </template>
          </el-form-item>
        </el-col>
      </el-row>
    </el-form>

需要嵌套的其他内容可以通过slot加入

多个选项可以通过ref动态获取,

:ref="
  (el) =>
    complexSelectorRef(el, `complexSelectorRef_${item.prop}`)
"

let complexSelectorRefList = new Map()

const complexSelectorRef = (el: any, key: string) => {
  if (el) {
    complexSelectorRefList.set(key,el) 
  }
};
    
// 赋值 || 重置 || 提交
Object.keys(complexSelectorCustomRefList).forEach(function (key) {
    res.push(complexSelectorCustomRefList[key].queryReset());
});
  

通过map结构来维护,key,value的结构可以很方便的通过模块的唯一标识来获取组件ref的值

后续可以通过promise来进行赋值,重置,提交。

数据绑定

前期本来打算直接通过v-model,提交,重置,都通过一个值去进行维护,过程中不需要思考其他的问题。 但是如果是多级嵌套,组件的渲染顺序上会存在一定的影响。

为了方便控制提交重置,使用promise来进行流程进度的维护。

每个子组件内部通过独立的对象来进行数据绑定,具有一定的隔离性,由外部统一重置,赋值。

以提交为例:

子组件内部
const doSubmit = () => {
  return new Promise((res, rej) => {
    if (props.item) {
      res({
        [props.item.prop]: transfer.value
      });
    }
  });
};

基础表单中集合数据

Object.keys(complexSelectorCustomRefList).forEach(function (key) {
    res.push(complexSelectorCustomRefList.get(key).doSubmit());
});
return new Promise((resolve, rej) => {
    Promise.all(res).then((e) => {
      resolve(e);
    });
});

题外话vue3组件渲染顺序

父组件:beforeCreate -> created -> beforeMount -> 子组件beforeCreate -> 子组件created -> beforeMount -> 子组件mounted -> 父组件mounted

扩展性质

页面由数据驱动,所以需要维护好一个清晰明了的数据结构,用来渲染生成表单。后续手动勾选添加表单内容,或者接入配置页面都需要通过维护渲染数据来进行

image.png

JSX版本灵活性更高后面尝试改成JSX