vue2+ant-design-vue 的form多表单组件及多Descriptions详情组件封装(可实现单页面有多个表单/详情按模块的方式展示并且可以收缩)

一、最终效果

二、组件集成了以下功能

json 复制代码
1、可以多模块配置form表单------配置formOpts对象
2、每个模块可以收起或展开------模块不设置title值取消此功能(或者设置disabled:true)
3、每个模块可以自定义插槽设置
4、头部标题可以显示隐藏------有title则显示没有则隐藏
5、可以自定义设置footerBtn操作按钮(默认:表单显示取消和保存按钮;详情显示取消按钮)------设置 :footerBtn="null"
6、多表单校验不通过可以指定哪个模块
7、可以设置tabs(默认展示第一个tab;可以指定展示某一个根据setSelectedTab方法)
8、头部返回操作默认返回上一页,若需要自定义可以设置isGoBackEvent
9、多模块详情页面value值可以自定义插槽
10、多模块详情页面value值可以自定义tip(提示)及提示icon自定义
11、多模块表单或详情页面如果不使用手风琴收缩功能可以设置"disabled:true"

三、实际组件是以下组件结合,并继承其Attributes、event、slot

1、多模块表单是基于我之前封装的 t-antd-form组件

2、多模块详情是基于我之前封装的 t-antd-detail组件

四、参数配置

1、代码示例

html 复制代码
<!-- 第一种 表单形式 -->
<t-module-form
    title="基本使用"
    ref="sourceForm"
    :formOpts="formOpts"
    :submit="submit"
/>
<!-- 第二种详情展示 根据handleType-->
<t-module-form
  title="模块详情--基本使用"
  ref="sourceDetail"
  handleType="desc"
  :descData="descData"
      />

2、配置参数(Attributes)继承a-page-header、TAntdForm、TAntdDetail组件属性、插槽、事件

参数 说明 类型 默认值
title 头部返回按钮标题 string
subTitle 头部副标题 string
extra 操作区,位于 title 行的行尾(右侧) slot
footerBtn 底部操作区(默认展示"取消/保存"按钮;使用插槽则隐藏)footerBtn="null"时隐藏底部操作 String/slot
isTabMargin tabs是否跟模块分离 Boolean false
tabMarginNum tabs跟模块分离距离 Number 10
tabs 页面展示是否需要页签(并且 tabs 的 key 是插槽) Array
isShowBack 是否显示返回icon Boolean false
isGoBackEvent 点击头部返回(默认返回上一页,设置此值可以自定义 back 事件) Boolean false
handleType 显示方式(edit 表 form 表单操作,desc 表详情页面) string edit
----edit handleType=edit 表 form 表单操作的属性 - -
------formOpts 表单配置描述,支持多分组表单 Object
------submit 保存时(调用 saveHandle 方法 ),返回 promise 可自动显示 loading function 所有表单数据
-----desc handleType=desc 表详情页面的属性 - -
------descData 详情页面配置描述,支持多分组表 (handleType= desc 生效) Object

2-1、descData 配置参数

参数 说明 类型 默认值
title 详情标题(是否显示控制折叠面板功能) String
slotName 插槽(自定义详情数据)有插槽就无需配置 data slot
name 每组详情定义的名字(作用:是否默认展开) String
disabled 禁用时取消收缩功能及隐藏 icon) Array false
descColumn 布局一行显示几列(默认:一行显示 4 列) Number 4
dataList 开启 filters 时详情接口返回的数据 Object {}
listTypeInfo 开启 filters 时下拉数据源 Object {}
data 详情配置项 Object
----label 详情字段说明标题 String -
----value 详情字段返回值 String -
----fieldName value 返回值的字段 String -
----slotName 插槽(自定义 value) slot -
----span 占用的列宽,默认占用 1 列,最多 4 列 Number 1
----tooltip value 值的提示语 String/function -
----iconClass tooltip 提示语的 icon String 'exclamation-circle'
----style tooltip 提示语的 icon的样式 Object -
----filters 字典类型(即后台返回的是数字类型)过滤转成中文 Object -
-------list 字典 list 定义的数据名即 listTypeInfo 里面对应的值 String -
-------key 下拉数据源的 key 字段 String 'value'
-------label 下拉数据源的 label 字段 String 'label'

2-2、formOpts 配置参数

参数 说明 类型 默认值
title 表单标题(是否显示控制折叠面板功能) String
slotName 插槽(自定义表单数据)有插槽就无需配置 opts slot
name 每组表单定义的名字(作用:是否默认展开) String
widthSize 每行显示几个输入项(默认两项) 最大值 4 Number 3
disabled 禁用时取消收缩功能及隐藏 icon) Boolean false
opts 表单配置项 Object

2-2-1、opts 配置参数(继承TAntdForm的所有属性)

参数 说明 类型 默认值
layout 改变表单项 label 与输入框的布局方式(默认:horizontal) /vertical String 'horizontal'
widthSize 每行显示几个输入项(默认两项) 最大值 4 Number 2
isTrim 全局是否开启清除前后空格(comp 为 a-input 且 type 不等于'password') Boolean true
formOpts 表单配置项 Object {}
---listTypeInfo 下拉选择数据源(type:'select'有效) Object {}
---fieldList form 表单每项 list Array []
------isHideItem 某一项不显示 Boolean false
------slotName 自定义表单某一项输入框 slot -
------childSlotName 自定义表单某一下拉选择项子组件插槽(a-select-option) slot -
------comp form 表单每一项组件是输入框还是下拉选择等(可使用第三方 UI 如 a-select/a-input 也可以使用自定义组件) String -
------formItemBind 表单每一项属性(继承FormModelItem的 Attributes) Object {}
------bind 表单每一项属性(继承第三方 UI 的 Attributes,如 a-input 中的 allowClear 清空功能)默认清空及下拉过滤 Object {}
------isTrim 是否不清除前后空格(comp 为 a-input 且 type 不等于'password') Boolean false
------type form 表单每一项类型 String -
------widthSize form 表单某一项所占比例(如果占一整行则设置 1) Number 2
------width form 表单某一项所占实际宽度 String 100%
------arrLabel type=select-arr 时,每个下拉显示的中文 String 'label'
------arrKey type=select-arr 时,每个下拉显示的中文传后台的数字 String 'value'
------label form 表单每一项 title String -
------labelRender 自定义某一项 title function -
------value form 表单每一项传给后台的参数 String -
------rules 每一项输入框的表单校验规则 Object/Array -
------list 下拉选择数据源(仅仅对 type:'select'有效) String -
------event 表单每一项事件标志(handleEvent 事件) String -
------eventHandle 继承 comp 组件的事件(返回两个参数,第一个自己自带,第二个 formOpts) Object -
------isSelfCom 是否使用自己封装的组件(TAntdSelect等---含有下拉框) Boolean false
---formData 表单提交数据(对应 fieldList 每一项的 value 值) Object -
---labelCol label 宽度({ span:2}) Object {span:2}
---wrapperCol 输入框 宽度 Object {span:22}
---rules 规则(可依据 AntdUI FormModel 配置------------对应 formData 的值) Object/Array -
---operatorList 操作按钮 list Array -

3、events

事件名 说明 返回值
handleEvent 单个查询条件触发事件 fieldList 中的 event 值和对应输入的 value 值
tabsChange 点击 tab 切换触发 被选中的标签 tab 实例
validateError 校验失败抛出事件 obj------每个收缩块的对象
back 头部标题点击返回事件 -

4、Methods

事件名 说明 返回值
resetFormFields 重置表单 -
clearValidate 清空校验 -
setSelectedTab 默认选中 tab 默认选中 tab 插槽名
saveHandle 异步 form 表单校验,生成 submit 属性(是个 function 并返回所有表单数据) 校验通过触发submit并返回Promise值

五、源码

1、TAntdModuleForm源码

html 复制代码
<template>
  <div class="t_antd_module_form" :style="{marginBottom:footerBtn!==null?'60px':''}">
    <div class="scroll_wrap">
      <a-page-header
        :title="title"
        :sub-title="subTitle"
        @back="back"
        v-bind="{ghost:false,...$attrs}"
        :class="{'isShowBack':isShowBack}"
      >
        <template v-for="(index, name) in $slots" v-slot:[name]>
          <slot :name="name" />
        </template>
        <template v-for="(index, name) in $scopedSlots" v-slot:[name]="data">
          <slot :name="name" v-bind="data"></slot>
        </template>
      </a-page-header>
      <!-- 表单页面 -->
      <AntdModuleForm v-if="handleType==='edit'" v-bind="$attrs" v-on="$listeners" ref="tAntdForm">
        <template v-for="(index, name) in $slots" v-slot:[name]>
          <slot :name="name" />
        </template>
        <template v-for="(index, name) in $scopedSlots" v-slot:[name]="data">
          <slot :name="name" v-bind="data"></slot>
        </template>
      </AntdModuleForm>
      <!-- 详情页面 -->
      <AntdModuleDetail v-else v-bind="$attrs">
        <template v-for="(index, name) in $slots" v-slot:[name]>
          <slot :name="name" />
        </template>
        <template v-for="(index, name) in $scopedSlots" v-slot:[name]="data">
          <slot :name="name" v-bind="data"></slot>
        </template>
      </AntdModuleDetail>
      <div class="tabs" v-if="tabs" :style="{'margin-top':isTabMargin?`${tabMarginNum}px`:0}">
        <a-tabs
          v-if="tabs&&tabs.length > 1"
          :default-active-key="tabs[0].key"
          v-model="activeName"
          @change="(activeKey) => $emit('tabsChange', activeKey)"
          :animated="false"
        >
          <a-tab-pane v-for="tab in tabs" :key="tab.key" :tab="tab.title">
            <slot :name="tab.key"></slot>
          </a-tab-pane>
        </a-tabs>
        <slot v-else :name="tabs&&tabs[0].key"></slot>
      </div>
      <slot name="default"></slot>
    </div>
    <footer class="handle_wrap" v-if="footerBtn!==null">
      <slot name="footerBtn" />
      <div v-if="!$slots.footerBtn">
        <a-button @click="back">取消</a-button>
        <a-button
          type="primary"
          v-if="handleType==='edit'"
          @click="saveHandle"
          :loading="loading"
        >{{btnTxt}}</a-button>
      </div>
    </footer>
  </div>
</template>
<script>
import { PageHeader, Button, Tabs } from 'ant-design-vue'
import AntdModuleDetail from './antdModuleDetail'
import AntdModuleForm from './antdModuleForm'
export default {
  name: 'TAntdModuleForm',
  components: {
    'a-page-header': PageHeader,
    'a-button': Button,
    'a-tabs': Tabs,
    'a-tab-pane': Tabs.TabPane,
    AntdModuleDetail,
    AntdModuleForm
  },
  props: {
    handleType: {
      type: String,
      default: 'edit' // edit表form表单操作,desc表详情页面
    },
    // 是否显示返回箭头
    isShowBack: {
      type: Boolean,
      default: false
    },
    // 返回上一层触发方法
    isGoBackEvent: {
      type: Boolean,
      default: false
    },
    // 操作按钮文字
    btnTxt: {
      type: String,
      default: '保存'
    },
    // tabs是否跟模块分离
    isTabMargin: {
      type: Boolean,
      default: false
    },
    // tabs跟模块分离距离(默认10px)
    tabMarginNum: {
      type: Number,
      default: 10
    },
    // 是否显示底部操作按钮 :footerBtn="null"
    footerBtn: Object,
    title: String,
    subTitle: String,
    tabs: Array,
    getContainer: Function,
    submit: Function
  },
  data() {
    return {
      activeName: this.tabs && this.tabs[0].key,
      loading: false
    }
  },
  methods: {
    // 获取默认选中tab
    setSelectedTab(key) {
      this.activeName = key
    },
    async saveHandle() {
      const self = this
      let form = {}
      let formError = {}
      let formOpts = {}
      let successLength = 0
      this.loading = true
      // 过滤非插槽表单
      Object.keys(self.$attrs.formOpts).forEach((key) => {
        if (self.$attrs.formOpts[key].opts) {
          formOpts[key] = self.$attrs.formOpts[key]
        }
      })
      await Object.keys(formOpts).forEach(async (formIndex) => {
        const { valid, formData } = await self.$refs.tAntdForm.$refs[formIndex][0].validate()
        console.log('formData--', formData)
        if (valid) {
          successLength = successLength + 1
          // form[formIndex] = self.$attrs.formOpts[formIndex].opts.formData
          form[formIndex] = formData
        }
      })
      if (successLength === Object.keys(formOpts).length) { // 所有表单都校验成功
        await this.submit(form)
        this.loading = false
        return true
      } else {
        // 校验失败抛出事件
        Object.keys(formOpts).forEach((key) => {
          if (Object.keys(form).length > 0) {
            Object.keys(form).map((val) => {
              if (key !== val) {
                formError[key] = formOpts[key]
              }
            })
          } else {
            formError[key] = formOpts[key]
          }
        })
        this.$emit('validateError', formError)
        this.loading = false
        return false
      }
    },
    back() {
      if (this.isShowBack) {
        return
      }
      this.$emit('back')
      if (!this.isGoBackEvent) {
        this.$router.go(-1)
      }
    },
    show(formType) {
      this.$nextTick(() => {
        this.updateFormFields()
        this.formType = formType
      })
    },
    // 清空表单
    resetFormFields() {
      const self = this
      let formOpts = {}
      // 过滤非插槽表单
      Object.keys(self.$attrs.formOpts).forEach((key) => {
        if (self.$attrs.formOpts[key].opts) {
          formOpts[key] = self.$attrs.formOpts[key]
        }
      })
      Object.keys(formOpts).forEach(formIndex => {
        self.$refs.tAntdForm.$refs[formIndex][0].resetFields()
      })
    },
    // 清空校验规则
    clearValidate() {
      const self = this
      let formOpts = {}
      // 过滤非插槽表单
      Object.keys(self.$attrs.formOpts).forEach((key) => {
        if (self.$attrs.formOpts[key].opts) {
          formOpts[key] = self.$attrs.formOpts[key]
        }
      })
      Object.keys(formOpts).forEach(formIndex => {
        self.$refs.tAntdForm.$refs[formIndex][0].clearValidate()
      })
    },
    updateFormFields() {
      const self = this
      let formOpts = {}
      // 过滤非插槽表单
      Object.keys(self.$attrs.formOpts).forEach((key) => {
        if (self.$attrs.formOpts[key].opts) {
          formOpts[key] = self.$attrs.formOpts[key]
        }
      })
      Object.keys(formOpts).forEach(formIndex => {
        self.$refs.tAntdForm.$refs[formIndex][0].updateFields(false)
      })
    },
    isShow(name) {
      return Object.keys(this.$slots).includes(name)
    }
  }
}
</script>
<style lang="scss">
.t_antd_module_form {
  display: flex;
  flex-grow: 1;
  flex-direction: column;
  height: 100%;
  text-align: left;
  background-color: #f0f2f5;
  overflow: auto;
  .scroll_wrap {
    display: flex;
    flex-direction: column;
    flex-grow: 1;
    .t_antd-form {
      .ant-collapse-borderless {
        background-color: #f6f6f6;
        .noTitle {
          .ant-collapse-header {
            display: none;
          }
        }
        .ant-collapse-item {
          background-color: #fff;
          margin-top: 10px;
          border: none;
          .ant-collapse-header {
            border-bottom: 1px solid #ebeef5;
          }
          .ant-collapse-content-box {
            padding: 16px;
          }
        }
      }
    }
    // 是否显示返回箭头
    .isShowBack {
      .ant-page-header-back {
        display: none;
      }
    }
    .tabs {
      padding: 0;
      margin: 0;
      .ant-tabs {
        background-color: #fff;
        .ant-tabs-bar {
          margin: 0;
          padding: 0 10px;
        }
        .ant-tabs-content {
          padding: 10px;
          .ant-tabs-tabpane {
            margin-top: 10px;
          }
        }
      }
    }
  }
  .handle_wrap {
    z-index: 4;
    right: 0;
    bottom: 0px;
    height: 60px;
    display: flex;
    align-items: center;
    justify-content: flex-end;
    background-color: #fff;
    border-top: 1px solid #ebeef5;
    text-align: right;
    width: 100%;
    .ant-btn + .ant-btn {
      margin-left: 12px;
    }
    .ant-btn:last-child {
      margin-right: 15px;
    }
  }
}
</style>

2、antdModuleForm源码

html 复制代码
<template>
  <div class="t_antd-form">
    <a-collapse :bordered="false" :defaultActiveKey="defaultActiveKey">
      <a-collapse-panel
        v-for="(formOpt, formIndex) in formOpts"
        :class="[formOpt.className,{ noTitle: !formOpt.title,disabledStyle:formOpt.disabled }]"
        :key="formIndex"
      >
        <template #header>
          {{formOpt.title}}
          <div class="t_btn" v-if="formOpt.btn">
            <slot :name="formOpt.btn"></slot>
          </div>
        </template>
        <template v-if="formOpt.slotName">
          <slot :name="formOpt.slotName"></slot>
        </template>
        <t-antd-form
          v-else
          :ref="formIndex"
          :formOpts="formOpt.opts"
          :ref-obj.sync="formOpt.ref"
          v-bind="formOpt.opts.layout === 'vertical'?{...$attrs}:{ labelCol: { span: 4 },wrapperCol: { span: 20 },...$attrs}"
          v-on="$listeners"
          @handleEvent="(val,type)=>$emit('handleEvent',val,type)"
        >
          <template v-for="(index, name) in $slots" v-slot:[name]>
            <slot :name="name" />
          </template>
          <template v-for="(index, name) in $scopedSlots" v-slot:[name]="data">
            <slot :name="name" v-bind="data"></slot>
          </template>
        </t-antd-form>
      </a-collapse-panel>
    </a-collapse>
  </div>
</template>
<script>
import { Collapse } from 'ant-design-vue'
export default {
  name: 'AntdModuleForm',
  components: {
    'a-collapse': Collapse,
    'a-collapse-panel': Collapse.Panel
  },
  props: {
    formOpts: {
      type: Object,
      default: () => ({})
    }
  },
  computed: {
    defaultActiveKey() {
      return Object.keys(this.formOpts)
    }
  }
}
</script>
<style lang="scss">
.t_antd-form {
  .ant-collapse-borderless {
    background-color: #f6f6f6;
    .noTitle {
      .ant-collapse-header {
        display: none;
      }
    }
    .ant-collapse-item {
      background-color: #fff;
      margin-top: 10px;
      border: none;
      .ant-collapse-header {
        border-bottom: 1px solid #ebeef5;
        font-weight: bold;
        color: #303133;
        cursor: pointer;
        display: flex;
        align-items: center;
        justify-content: space-between;
        .t_btn {
          margin-right: 15px;
        }
      }
      .ant-collapse-content-box {
        padding: 16px;
        .ant-form-inline {
          .ant-form-item {
            margin: 0;
          }
        }
      }
    }
    // 禁用时取消收缩功能及隐藏icon
    .disabledStyle {
      .ant-collapse-header {
        color: #303133;
        cursor: default;
        padding-left: 20px;
        display: flex;
        align-items: center;
        justify-content: space-between;
        font-weight: bold;
        pointer-events: none;
        .ant-collapse-arrow {
          display: none;
        }
        .t_btn {
          margin-right: 15px;
          pointer-events: none;
          .ant-btn {
            pointer-events: auto;
          }
        }
      }
    }
  }
}
</style>

3、antdModuleDetail源码

html 复制代码
<template>
  <div class="t_antd_module_detail">
    <a-collapse :bordered="false" :defaultActiveKey="defaultActiveKey">
      <a-collapse-panel
        v-for="(val, index) in descData"
        :class="{ noTitle: !val.title,disabledStyle:val.disabled }"
        :key="index"
      >
        <template #header>
          {{val.title}}
          <div class="t_btn" v-if="val.btn">
            <slot :name="val.btn"></slot>
          </div>
        </template>
        <template v-if="val.slotName">
          <slot :name="val.slotName"></slot>
        </template>
        <t-antd-detail v-else :descData="val.data" v-bind="$attrs">
          <template v-for="(index, name) in $slots" v-slot:[name]>
            <slot :name="name" />
          </template>
          <template v-for="(index, name) in $scopedSlots" v-slot:[name]="data">
            <slot :name="name" v-bind="data"></slot>
          </template>
        </t-antd-detail>
      </a-collapse-panel>
    </a-collapse>
  </div>
</template>
<script>
import { Collapse } from 'ant-design-vue'
export default {
  name: 'AntdModuleDetail',
  components: {
    'a-collapse': Collapse,
    'a-collapse-panel': Collapse.Panel
  },
  props: {
    descData: {
      type: Object,
      default: () => ({})
    }
  },
  computed: {
    defaultActiveKey() {
      return Object.keys(this.descData)
    }
  }
}
</script>
<style lang="scss">
.t_antd_module_detail {
  .ant-collapse-borderless {
    background-color: #f6f6f6;
    .noTitle {
      .ant-collapse-header {
        display: none;
      }
    }
    .ant-collapse-item {
      background-color: #fff;
      margin-top: 10px;
      border: none;
      .ant-collapse-header {
        border-bottom: 1px solid #ebeef5;
        font-weight: bold;
        color: #303133;
        cursor: pointer;
        display: flex;
        align-items: center;
        justify-content: space-between;
        .t_btn {
          margin-right: 15px;
        }
      }
      .ant-collapse-content-box {
        padding: 16px;
        .ant-form-inline {
          .ant-form-item {
            margin: 0;
          }
        }
      }
    }
    // 禁用时取消收缩功能及隐藏icon
    .disabledStyle {
      .ant-collapse-header {
        color: #303133;
        cursor: default;
        padding-left: 20px;
        display: flex;
        align-items: center;
        justify-content: space-between;
        font-weight: bold;
        pointer-events: none;
        .ant-collapse-arrow {
          display: none;
        }
        .t_btn {
          margin-right: 15px;
          pointer-events: none;
          .ant-btn {
            pointer-events: auto;
          }
        }
      }
    }
  }
}
</style>

六、组件地址

gitHub组件地址

gitee码云组件地址

七、相关文章

基于ElementUi再次封装基础组件文档


基于ant-design-vue再次封装基础组件文档


vue3+ts基于Element-plus再次封装基础组件文档

相关推荐
小月鸭3 分钟前
如何理解HTML语义化
前端·html
jump68026 分钟前
url输入到网页展示会发生什么?
前端
诸葛韩信29 分钟前
我们需要了解的Web Workers
前端
brzhang35 分钟前
我觉得可以试试 TOON —— 一个为 LLM 而生的极致压缩数据格式
前端·后端·架构
yivifu1 小时前
JavaScript Selection API详解
java·前端·javascript
这儿有一堆花1 小时前
告别 Class 组件:拥抱 React Hooks 带来的函数式新范式
前端·javascript·react.js
十二春秋1 小时前
场景模拟:基础路由配置
前端
六月的可乐1 小时前
实战干货-Vue实现AI聊天助手全流程解析
前端·vue.js·ai编程
一 乐2 小时前
智慧党建|党务学习|基于SprinBoot+vue的智慧党建学习平台(源码+数据库+文档)
java·前端·数据库·vue.js·spring boot·学习
BBB努力学习程序设计2 小时前
CSS Sprite技术:用“雪碧图”提升网站性能的魔法
前端·html