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

news2024/11/26 20:20:27

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

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

做了优化,效果如下:
在这里插入图片描述
废话不多说,直接上代码

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/编辑api
demo.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>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/615900.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【技术选型】时序数据库选型

文章目录 1、前言2、概述2.1 时序数据库的定义2.2 时序数据库的概念2.3 时序数据库的趋势 3、时序数据库对比3.1 influxdb3.2 Prometheus3.3 TDengine3.4 DolphinDB 4、选型结论 1、前言 时序数据治理是数据治理领域核心、打通IT与OT域数据链路&#xff0c;是工业物联网基石、…

Macos中Automator自动操作实现文本加解密、Json格式化、字数统计等操作

文章目录 一、说明打开Automator效果 二、文本加密三、文本解密四、Json格式化五、字数统计 一、说明 在 Automator 的工作流程中&#xff0c;动作是按照从上到下的顺序执行的&#xff0c;每一个动作的输出默认会成为下一个动作的输入。 打开Automator Command 空格 &#…

万万没想到系列,世界上最知名的失败建筑设计合集!

​ 大家好&#xff0c;这里是建模助手。 我们生活在由建筑包围的世界里&#xff0c;生活的面貌造就了建筑的多样性。而矗立的建筑也无言的记录着时代&#xff0c;尤其是一些建筑大师们的作品&#xff0c;可谓是集艺术和美学于一体的一流名作。 但&#xff0c;这不是凡事都有例…

虚拟内存原理介绍

文章目录 1. 虚拟内存介绍2. 虚拟寻址3. 虚拟地址空间3. 页表4. 地址翻译5. TLB加速地址翻译6. 多级页表7. 页面置换算法 1. 虚拟内存介绍 我们知道系统中的所有进程都是共享CPU和主存资源&#xff0c;但这样就会存在一个问题&#xff0c;这么多进程怎么知道主存上的一块内存是…

使用Java语言开发高效易用的--雅书阁商城管理系统

使用Java语言开发雅书阁商城管理系统 如果你正在寻找一种简单而优雅的方式来管理图书&#xff0c;那么使用Java语言开发雅书阁商城管理系统就是一个好选择。下面我们来详细介绍这个系统的开发过程。 效果展示 1.首页 2.注册界面 3.登录成功商城首页 4.购物车 5.电子书…

避开测试开发的常见陷阱:一份实战指南

陷阱一&#xff1a;过度依赖自动化测试 过度依赖自动化测试可能导致对复杂的用户交互和体验不够重视。自动化测试的力量在于它的一致性和覆盖广泛的可能性&#xff0c;但人工测试也同样重要&#xff0c;尤其是对于用户体验和复杂的用户交互。 示例&#xff1a;在一个电商网站…

ROS和ROS2使用

ubuntu20.04下安装qt5.12 https://blog.csdn.net/lj19990824/article/details/121013721 Ubuntu 20.04在桌面左侧边栏添加QT creator快捷图标 https://blog.csdn.net/kavieen/article/details/118695038 Qt和ROS&#xff1a;https://github.com/chengyangkj?tabrepositories…

操作系统原理 —— 内存覆盖与交换(十九)

什么情况下需要覆盖与交换 要弄清楚什么是覆盖与交换的概念&#xff0c;首先我们要知道在什么情况下才会使用到覆盖与交换。 在早期的计算机内存很小的时候&#xff0c;比如 IBM 推出的第一台 PC 机最大只支持 1 MB 大小的内存&#xff0c;因此会经常出现内存大小不够的情况&…

c++函数重载与运算符重载基础

什么是重载 重载&#xff0c;简单说&#xff0c;就是函数或者方法有相同的名称&#xff0c;但是参数列表不相同的情形&#xff0c;这样的同名不同参数的函数或者方法之间&#xff0c;互相称之为重载函数或者方法。 重载的作用&#xff1a;重载函数常用来实现功能类似而所处理的…

【C语言】数组和字符串

目录 数组和字符串 概述 一维数组 一维数组的定义和使用 一维数组的初始化 数组名 二维数组 字符数组与字符串 字符数组与字符串区别 数组和字符串 概述 在程序设计中&#xff0c;为了方便处理数据把具有相同类型的若干变量按有序形式组织起来——称为数组。 数组就…

紧接上文,基于轻量级yolov5s模型开发构建手写甲骨文检测识别系统

在我之前的文章中&#xff0c;关于手写文字、手写数字、手写字母的检测识别相关的项目都有了不少的实践了&#xff0c;这里就不在赘述了&#xff0c;感兴趣的话可以自行移步阅读即可。 《基于轻量级目标检测模型实现手写汉字检测识别计数》 《python开发构建基于机器学习模型…

【ICEM CFD】导入模型后,即使勾选point和curve也看不到几何模型上的点和线

一、问题背景 导入模型后&#xff0c;即使勾选point和curve也看不到几何模型上的点和线。 二、解决办法 原来导入模型后&#xff0c;往往第一步最需要操作的是&#xff01;&#xff01;&#xff01; 构建拓扑&#xff01;&#xff01;&#xff01; Build Diagnostic Topolo…

完美解决safari、微信浏览器下拉回弹效果、包含微信小程序 webview 套H5页面下拉效果。

如题&#xff0c;解决微信小程序、公众号 下拉回弹橡皮筋效果&#xff0c; 屏蔽掉 “此网页由XXXXX提供”; // 禁止页面上下整体滑动 document.body.style.overflow "hidden"

基于Jmeter+ant+Jenkins+钉钉机器人群通知的接口自动化测试

前言 搭建jmeterantjenkins环境有些前提条件&#xff0c;那就是要先配置好java环境&#xff0c;本地java环境至少是JDK8及以上版本&#xff0c;最好是JAVA11或者JAVA17等较高的java环境&#xff0c;像jenkins这种持续构建工具基本都在向上兼容JAVA的环境&#xff0c;以前的JAV…

为什么网络安全人口很稀缺,招聘人数却很少?

2020年我国网络空间安全人才数量缺口超过了140万&#xff0c;就业人数却只有10多万&#xff0c;缺口高达了93%。这里就有人会问了&#xff1a; 1、网络安全行业为什么这么缺人&#xff1f; 2、明明人才那么稀缺&#xff0c;为什么招聘时招安全的人员却没有那么多呢&#xff1…

常见数据库(MSSQL,Mysql,PostgreSQL,Oracle)安装注意事项

常见数据库安装注意事项 &#xff08;原标题: DataWindowHTTP数据库安装&#xff09; 转载请保留版权消息勿删除&#xff1a;&#xff08;谢绝转载到任何文档网站&#xff01;&#xff09; blog.csdn.net/chengg0769 http://www.powerbuilder.ltd http://www.haojiaocheng.…

设计模式(行为型模式)之:Observer(观察者模式)

文章目录 动机使用场景代码实现类图结构模式分析&#xff1a; 动机 在软件构建过程中&#xff0c;我们需要为某些对象建立一种“通知依赖关系” - 一个对象&#xff08;目标对象&#xff09;的状态改变&#xff0c;所有的依赖对象&#xff08;观察者对象&#xff09;都将得到通…

绝不能错过!8款AI文案神器,让你轻松写出优质文案

无论你是否准备好&#xff0c;它们都已经来了。如果你知道如何使用它们&#xff0c;AI文案工具可以成为你的新朋友。 现在AI文案工具无处不在&#xff0c;眼花缭乱&#xff0c;从内容生成器到电子商务聊天机器人。原因很简单&#xff1a;AI可以节省大量时间和金钱。这是我们都喜…

markdown 编辑器使用

在博客开头加上 [TOC](这是你的目录标题)就可以根据博客内容自动生成如下所示的目录&#xff1a; 这是你的目录标题 Markdown 编辑器功能快捷键合理的创建标题&#xff0c;有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表无序…

智慧城市的建设需要数字孪生技术吗?

智慧城市建设需要依靠多种技术来实现数字化、智能化和可持续发展的目标。其中&#xff0c;数字孪生技术在智慧城市建设中起着重要的作用。 首先&#xff0c;数字孪生技术可以提供高度精确的城市建筑和基础设施的数字模型。通过对城市的建筑、道路、水系等要素进行数字化建模&a…