最近有小伙伴在问动态表单如何再次封装,比如配合模态框或者抽屉封装多一层,这样可以大大提高开发效率,结合之前的写的

vue2 + antd 封装动态表单组件(一)vue2 + antd 封装动态表单组件(二)

做了优化,效果如下:

vue3 antdesgin table转换为手机端_vue


废话不多说,直接上代码

1.封装输入框组件,显示可输入的长度,其他自定义组件可采用类似的方法;

my-input.vue

<template>
  <div class="my-input">
    <a-input ref="myInput" v-on="$listeners" v-bind="$attrs" :maxLength="max" v-model="val">
      <template v-for="(value, name) in $slots" #[name]="slotData">
        <slot :name="name" v-bind="slotData || {}"></slot>
      </template>
    </a-input>
    <div class="my-input-count">
      <!-- {{ $attrs.value && $attrs.value.length ? $attrs.value.length : 0 }}/{{max}} -->
      {{ value.length }} / {{ max }}
    </div>
  </div>
</template>
<script>
export default {
  model: {
    prop: "value",
    event: "change",
  },
  props: {
    value: {
      type: String,
      default: ""
    },
    max: {
      type: Number,
      default: 20,
    },
  },
  watch: {
    value(newVal) {
       this.val = newVal
    }
  },
  data() {
    return {
        val: this.$attrs['data-__meta'].initialValue
    }
  },
};
</script>
<style lang="less" scoped>
.my-input {
  position: relative;
  display: flex;
  /deep/.ant-input {
    padding-right: 45px;
  }
}
.my-input-count {
  position: absolute;
  right: 8px;
  top: -4px;
  color: #909399;
  font-size: 12px;
  transition: color 0.3s;
}
</style>

2.封装动态表单组件,已解决了自定义表单项,表单联动,异步获取数据,属性透传等问题,其他比如表单验证,事件透传,样式等问题小伙伴们也可以思考思考,顺便优化优化;

dynamic-form.vue

<template>
  <a-spin :spinning="loading">
    <a-form
      :form="form"
      ref="form"
      :label-col="labelCol"
      :wrapper-col="wrapperCol"
    >
      <div v-for="(field, fieldIndex) in fieldItemOptions" :key="field.key">
        <a-form-item
          :label="field.label"
          :required="field.required"
          v-if="!field.hidden"
        >
          <!-- text文本框 -->
          <template v-if="field.type === 'text'">
            <MyInput
              v-decorator="[
                field.key,
                {
                  rules: [
                    {
                      required: field.required,
                      message: `${field.label}不能为空`,
                    },
                  ],
                  initialValue: field.value,
                },
              ]"
              :placeholder="`请输入${field.label}`"
              :disabled="field.disabled"
              :max="field.max"
              v-bind="field.props"
            ></MyInput>
          </template>

          <!-- textarea 文本域 -->
          <template v-else-if="field.type === 'textarea'">
            <a-textarea
              v-decorator="[
                field.key,
                {
                  rules: [
                    {
                      required: field.required,
                      message: `${field.label}不能为空`,
                    },
                  ],
                  initialValue: field.value,
                },
              ]"
              :placeholder="`请输入${field.label}`"
              :disabled="field.disabled"
              v-bind="{ ...field.props }"
            />
          </template>

          <!-- select下拉框 -->
          <template v-else-if="field.type === 'select'">
            <a-select
              :placeholder="`请选择${field.label}`"
              v-decorator="[
                field.key,
                {
                  rules: [
                    {
                      required: field.required,
                      message: `请选择${field.label}`,
                    },
                  ],
                  initialValue: field.value,
                },
              ]"
              v-bind="{ ...field.props }"
              @change="handleChange($event, field, fieldIndex)"
            >
              <a-select-option
                v-for="option in field.options"
                :key="option.value"
                :value="option.value"
                >{{ option.label }}</a-select-option
              >
            </a-select>
          </template>

          <!-- checkbox多选框 -->
          <template v-else-if="field.type === 'checkbox'">
            <a-checkbox-group
              v-decorator="[
                field.key,
                {
                  rules: [
                    {
                      required: field.required,
                      message: `请选择${field.label}`,
                    },
                  ],
                  initialValue: field.value,
                },
              ]"
              v-bind="{ ...field.props }"
              @change="handleChange($event, field, fieldIndex)"
            >
              <a-checkbox
                v-for="option in field.options"
                :key="option.value"
                :value="option.value"
                :style="{ width: field.width }"
                >{{ option.label }}</a-checkbox
              >
            </a-checkbox-group>
          </template>

          <!-- radio单选框 -->
          <template v-else-if="field.type === 'radio'">
            <a-radio-group
              v-decorator="[
                field.key,
                {
                  rules: [
                    {
                      required: field.required,
                      message: `请选择${field.label}`,
                    },
                  ],
                  initialValue: field.value,
                },
              ]"
              v-bind="{ ...field.props }"
              @change="handleChange($event, field, fieldIndex)"
            >
              <a-radio
                v-for="option in field.options"
                :key="option.value"
                :value="option.value"
                >{{ option.label }}</a-radio
              >
            </a-radio-group>
          </template>

          <template v-else-if="field.type === 'datePicker'">
            <a-date-picker
              :placeholder="`请选择${field.label}`"
              v-decorator="[
                field.key,
                {
                  rules: [
                    {
                      required: field.required,
                      message: `请选择${field.label}`,
                    },
                  ],
                  initialValue: field.value,
                },
              ]"
              v-bind="{ ...field.props }"
            >
            </a-date-picker>
          </template>
        </a-form-item>
      </div>
    </a-form>
  </a-spin>
</template>

<script>
import { deepClone } from "@/common/utils";
import MyInput from "./my-input";
export default {
  components: { MyInput },
  props: {
    // 表单域配置
    fieldOptions: {
      type: Array,
      default: () => [],
    },

    // 编辑时表单回显的默认数据
    model: {
      type: Object,
      default: () => ({}),
    },

    // 标签宽度
    labelCol: {
      type: Object,
      default: () => {
        return {
          xs: { span: 24 },
          sm: { span: 6 },
        };
      },
    },

    // 控件宽度
    wrapperCol: {
      type: Object,
      default: () => {
        return {
          xs: { span: 24 },
          sm: { span: 16 },
        };
      },
    },
  },

  computed: {},

  data() {
    return {
      loading: false,
      fieldItemOptions: [],
      fieldItemRelativeOptions: [],
      form: this.$form.createForm(this, { name: "dynamic-form" }),
    };
  },
  methods: {
    // 初始化表单,只初始化一次,相比之前watch监听的写法,这里优化了性能
    async initForm() {
      this.loading = true;
      const fieldOptions = deepClone(this.fieldOptions);
      for (let i = 0; i < fieldOptions.length; i++) {
        const c = fieldOptions[i];
        if (!c.props) c.props = {};
        c.value = this.model[c.key];
        for (const key in c) {
          if (c[key] && c[key] instanceof Function) {
            c[key] = await c[key](this.model);
          }
        }
      }
      this.fieldItemRelativeOptions = fieldOptions.filter(
        (c) => c?.relativeList?.length
      );
      this.fieldItemOptions = deepClone(fieldOptions);
      this.loading = false;
    },

    // 提交表单
    handleSubmit() {
      return new Promise((resolve, reject) => {
        this.form.validateFields((err, formData) => {
          if (err) {
            reject(err);
            return;
          }
          const formatFormData = this.formatFormData();
          for (const key in formatFormData) {
            formatFormData[key](formData);
          }
          // 提交表单逻辑
          console.log("表单数据:", formData);
          resolve(formData);
        });
      });
    },

    // 表单数据格式化
    formatFormData() {
      return {
        // datePicker类型
        datePicker: (formData) => {
          console.log('datePicker', formData);
          const type = this.fieldItemOptions.filter(
            (c) => c.type === "datePicker"
          );
          if (type.length) {
            type.forEach((c) => {
              formData[c.key] = formData[c.key].format('YYYY-MM-DD');
            });
          }
        },
        // 其他类型
      };
    },

    // 处理关联表单项,只处理关联项,相比之前写onValuesChange优化了性能
    handleChange(e, field, fieldIndex) {
      if (this.fieldItemRelativeOptions.length) {
        this.fieldItemRelativeOptions.forEach((c) => {
          if (c.key === field.key) {
            c.relativeList.forEach((d) => {
              const target = this.fieldOptions.find((k) => k.key === d.key);
              const targetIndex = this.fieldOptions.findIndex(
                (k) => k.key === d.key
              );
              d.props.forEach(async (x) => {
                this.fieldItemOptions[targetIndex][x] = await target[x](
                  this.form.getFieldsValue()
                );
              });
            });
          }
        });
      }
    },
  },
  mounted() {
    this.initForm();
  },
};
</script>

3.弹窗组件a-modal嵌套动态表单组件,抽屉组件a-drawer 也可采用类似的方法进行封装;
dynamic-form-modal.vue

<template>
  <a-modal
    :title="isEdit ? '编辑' : '新增'"
    :visible="visible"
    destroyOnClose
    @cancel="handleClose()"
    :bodyStyle="{ maxHeight: `calc(100vh - ` + 300 + `px)`, overflowY: 'auto' }"
    v-bind="$attrs"
  >
    <template slot="footer">
      <a-button @click="handleClose()">取消</a-button>
      <a-button type="primary" @click="handleSubmitDebounce()">
        <a-icon v-if="confirmLoading" type="loading"></a-icon>
        {{ confirmLoading ? "正在提交..." : $attrs.okText || "确定" }}
      </a-button>
    </template>
    <a-spin :spinning="confirmLoading">
      <dynamic-form
        ref="dynamicForm"
        :fieldOptions="fieldOptions"
        :labelCol="labelCol"
        :wrapperCol="wrapperCol"
        :model="model"
        :modelKey="modelKey"
      ></dynamic-form>
    </a-spin>
  </a-modal>
</template>
  
  <script>
import { debounce } from "@/common/utils";
import DynamicForm from "./dynamic-form";
export default {
  components: { DynamicForm },
  props: {
    // 表单域配置
    fieldOptions: {
      type: Array,
      default: () => [],
    },
    // 弹窗显示or隐藏
    visible: {
      type: Boolean,
      default: false,
    },
    // 编辑时表单回显的默认数据
    model: {
      type: Object,
      default: () => ({}),
    },
    // 判断编辑or新增的key
    modelKey: {
      type: String,
      default: "id",
    },
    // 标签宽度
    labelCol: {
      type: Object,
      default: () => {
        return {
          xs: { span: 24 },
          sm: { span: 6 },
        };
      },
    },
    // 控件宽度
    wrapperCol: {
      type: Object,
      default: () => {
        return {
          xs: { span: 24 },
          sm: { span: 16 },
        };
      },
    },
    // 新增api
    addApi: {
      type: Function,
      default: () => function () {},
    },
    // 编辑api
    updateApi: {
      type: Function,
      default: () => function () {},
    },
    // 补充的参数
    params: {
      type: Object,
      default: () => ({}),
    },
  },
  computed: {
    // 判断编辑or新增
    isEdit() {
      return !!this.model[this.modelKey];
    },
    // 操作文本
    actionText() {
      return this.isEdit ? "编辑" : "新增";
    },
  },
  data() {
    return {
      // 提交按钮加载状态
      confirmLoading: false,
      // 提交按钮防抖
      handleSubmitDebounce: debounce(this.handleOk, 500),
    };
  },
  methods: {
    // 模态框提交表单
    handleOk() {
      if (this.confirmLoading) {
        this.$message.info("正在提交表单,请稍后再操作");
        return;
      }
      this.$refs.dynamicForm
        .handleSubmit()
        .then(async (formData) => {
          // 新增参数和编辑参数
          this.confirmLoading = true;
          let resultParams = { ...formData, ...this.params };
          if (this.isEdit) {
            resultParams[this.modelKey] = this.model[this.modelKey];
          }
          // 新增api或编辑api
          console.log("弹窗参数", resultParams);
          let api = this.isEdit ? this.updateApi : this.addApi;
          api(resultParams)
            .then(() => {
              this.handleClose();
              this.$message.success(`${this.actionText}成功`);
              // 提交表单后的回调
              this.$emit("afterSubmit", resultParams);
            })
            .catch(() => {
              this.$message.error(`${this.actionText}失败`);
            })
            .finally(() => {
              this.confirmLoading = false;
            });
        })
        .catch(() => {});
    },

    // 关闭
    handleClose() {
      // 关闭弹窗的回调
      this.$emit("update:visible", false);
      this.$emit("close");
      let form = this.$refs.dynamicForm.form;
      // 延迟清空表单
      let timer = setTimeout(() => {
        form.resetFields();
        clearTimeout(timer);
        timer = null;
        form = null;
      }, 100);
    },
  },
};
</script>
<style lang='less' scoped>
</style>

4.使用该弹窗组件,注意使用时传封装好的新增api/编辑apidemo.vue

<template>
  <div style="display: flex; height: 100vh; width: 100vw">
    <div style="padding: 32px; border: 1px solid #ccc; margin: auto">
      <a-button style="margin-right: 32px" @click="openModal()">新增</a-button>
      <a-button type="primary" @click="openModal(record)">编辑</a-button>
    </div>
    <DynamicFormModal
      :fieldOptions="fieldOptions"
      :visible.sync="showModal"
      :model="model"
      width="50%"
      okText="提交"
      @afterSubmit="() => '提交表单后的操作,比如刷新table数据'"
    ></DynamicFormModal>
  </div>
</template>

<script>
import DynamicFormModal from "./dynamic-form-modal.vue";
export default {
  components: {
    DynamicFormModal,
  },
  data() {
    return {
      showModal: false,
      fieldOptions: [
        {
          label: "姓名",
          key: "name",
          value: "",
          type: "text",
          required: true,
          disabled: (formData) => formData.country === 2,
          max: 20,
        },
        {
          label: "性别",
          key: "sex",
          value: 1,
          type: "radio",
          required: true,
          options: [
            {
              value: 1,
              label: "男",
            },
            {
              value: 2,
              label: "女",
            },
          ],
        },
        {
          label: "生日",
          key: "birthday",
          value: null,
          type: "datePicker",
          required: true,
        },
        {
          label: "兴趣爱好",
          key: "hobby",
          value: [],
          type: "checkbox",
          required: true,
          options: [
            {
              value: 1,
              label: "足球",
            },
            {
              value: 2,
              label: "篮球",
            },
            {
              value: 3,
              label: "排球",
            },
          ],
          hidden: (formData) => formData.country === 2,
        },
        {
          label: "国家",
          key: "country",
          value: undefined,
          type: "select",
          required: true,
          options: async () => await this.getCountryList(),
          relativeList: [
            {
              key: "name",
              props: ["disabled"],
            },
            {
              key: "hobby",
              props: ["hidden"],
            },
            {
              key: "desc",
              props: ["required"],
            },
          ],
        },
        {
          label: "个人简介",
          key: "desc",
          value: "",
          type: "textarea",
          required: (formData) => formData.country === 2,
        },
      ],
      model: {},
      record: {
        id: 1,
        message: "我是数据源",
        name: "动态表单",
        sex: 2,
        hobby: [1, 2],
        country: 1,
        desc: "这是一个简单的例子",
      },
    };
  },
  methods: {
    openModal(record = {}) {
      this.model = { ...record };
      this.showModal = true;
    },

    // 模拟获取后台数据
    getCountryList() {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          const data = [
            {
              value: 1,
              label: "中国",
            },
            {
              value: 2,
              label: "美国",
            },
            {
              value: 3,
              label: "俄罗斯",
            },
          ];
          resolve(data);
        }, 1200);
      });
    },
  },
};
</script>