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再次封装基础组件文档

相关推荐
学习ing小白1 小时前
JavaWeb - 5 - 前端工程化
前端·elementui·vue
一只小阿乐1 小时前
前端web端项目运行的时候没有ip访问地址
vue.js·vue·vue3·web端
计算机学姐1 小时前
基于python+django+vue的旅游网站系统
开发语言·vue.js·python·mysql·django·旅游·web3.py
真的很上进1 小时前
【Git必看系列】—— Git巨好用的神器之git stash篇
java·前端·javascript·数据结构·git·react.js
胖虎哥er1 小时前
Html&Css 基础总结(基础好了才是最能打的)三
前端·css·html
qq_278063712 小时前
css scrollbar-width: none 隐藏默认滚动条
开发语言·前端·javascript
.ccl2 小时前
web开发 之 HTML、CSS、JavaScript、以及JavaScript的高级框架Vue(学习版2)
前端·javascript·vue.js
小徐不会写代码2 小时前
vue 实现tab菜单切换
前端·javascript·vue.js
2301_765347542 小时前
Vue3 Day7-全局组件、指令以及pinia
前端·javascript·vue.js
ch_s_t2 小时前
新峰商城之分类三级联动实现
前端·html