vue3+element-plus 的form多表单组件及多Descriptions详情组件封装(可实现单页面有多个表单/详情按模块的方式展示并且可以收缩;新增文档demo示例)

2023-09-20 优化模块表单组件;新增文档demo示例

一、最终效果

二、组件集成了以下功能

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

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

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

四、参数配置

1、代码示例


<t-module-form
  title="模块表单组件运用"
  ref="sourceForm"
  :formOpts="formOpts"
  :submit="submit"
/>

<t-module-form
  title="模块详情组件运用"
  ref="sourceDetail"
  handleType="desc"
  :descData="descData"
/>

2、配置参数(Attributes)继承 t-form/t-detail Attributes

参数 说明 类型 默认值
title 头部返回按钮标题 string
titleSlot 是否使用插槽显示 title Boolean false
subTitle 头部副标题 string
extra 操作区,位于 title 行的行尾(右侧) slot
footer 底部操作区(默认展示“取消/保存”按钮;使用插槽则隐藏)footer="null"时隐藏底部操作 String slot
isTabMargin tabs是否跟模块分离 Boolean false
tabMarginNum tabs跟模块分离距离 Number 10
tabs 页面展示是否需要页签(并且 tabs 的 key 是插槽)——只显示在最后一个模块下 Array
btnTxt 表单模块-‘保存按钮文字’ string ‘保存’
titleBold 模块Title文字是否加粗 Boolean false
isShowBack header不显示返回icon Boolean false
isGoBackEvent 点击头部返回(默认返回上一页,设置此值可以自定义 back 事件) Boolean false
handleType 显示方式(‘edit’:form 表单操作,‘desc’:表详情页面) string edit
----edit handleType=edit 表 form 表单操作的属性 - -
------formOpts 表单配置描述,支持多分组表单 Object
------submit 点击保存时返回所有表单数据(数据格式 promise 且可显示 loading 状态) function 所有表单数据
-----desc handleType=desc 表详情页面的属性 - -
------descColumn 详情页面展示每行显示几列(handleType= desc 生效) Number 4
------descData 详情页面配置描述,支持多分组表 (handleType= desc 生效) Object
2-1、formOpts 配置参数
参数 说明 类型 默认值
title 表单标题(是否显示控制折叠面板功能) String
slotName 插槽(自定义表单数据)有插槽就无需配置 opts slot
name 每组表单定义的名字(作用:是否默认展开) String
widthSize 每行显示几个输入项(默认两项) 最大值 4 Number 3
disabled 禁用时取消收缩功能及隐藏 icon) Boolean false
opts 表单配置项 Object
2-1-1、opts 配置参数
参数 说明 类型 默认值
rules 规则(可依据 element-plus el-form 配置————对应 formData 的值) Object/Array -
operatorList 操作按钮 list Array -
listTypeInfo 下拉选择数据源(type:'select’有效) Object -
labelPosition 改变表单项 label 与输入框的布局方式(默认:right) /top String right
labelWidth label 宽度(默认值 120px) String 120px
formData 表单提交数据(对应 fieldList 每一项的 value 值) Object -
fieldList form 表单每项 list Array -
----slotName 自定义表单某一项输入框 slot -
----comp form 表单每一项组件是输入框还是下拉选择等(可使用第三方 UI 如 el-select/el-input 也可以使用自定义组件) String -
----bind 表单每一项属性(继承第三方 UI 的 Attributes,如 el-input 中的 clearable 清空功能)默认清空及下拉过滤 Object -
----type form 表单每一项类型 String -
----widthSize form 表单某一项所占比例(如果一行展示可以设值:1) Number 2
----width form 表单某一项所占实际宽度 String -
----arrLabel type=select-arr 时,每个下拉显示的中文 String label
----arrKey type=select-arr 时,每个下拉显示的中文传后台的数字 String key
----label form 表单每一项 title String -
----labelRender 自定义某一项 title function -
----value form 表单每一项传给后台的参数 String -
----rules 每一项输入框的表单校验规则 Object/Array -
----list 下拉选择数据源(仅仅对 type:'select’有效) String -
----event 表单每一项事件标志(handleEvent 事件) String -
2-2、descData 配置参数
参数 说明 类型 默认值
title 详情标题(是否显示控制折叠面板功能) String -
slotName 插槽(自定义详情数据)有插槽就无需配置 data slot -
name 每组详情定义的名字(作用:是否默认展开) String -
disabled 禁用时取消收缩功能及隐藏 icon) Boolean false
data 详情配置项 Object -
----label 详情字段说明标题 String -
----value 详情字段返回值 String -
----slotName 插槽(自定义 value) slot -
----span 占用的列宽,默认占用 1 列,最多 4 列 Number 1
----tooltip value 值的提示语 String/function -

3、events

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

4、Methods

事件名 说明 参数
resetFormFields 重置表单 -
clearValidate 清空校验 -
setSelectedTab 默认选中 tab 默认选中 tab 插槽名

五、具体代码

<template>
  <div
    class="t_module_form"
    :style="{ marginBottom: footer !== null ? '60px' : '' }"
  >
    <div class="scroll_wrap">
      
      <el-page-header
        v-if="title || titleSlot"
        :title="title"
        @back="back"
        :class="{
          noContent: !subTitle,
          isShowBack: isShowBack,
        }"
      >
        <template #title v-if="titleSlot">
          <slot name="title">slot>
        template>
        <template #content>
          <div class="sub_title">{{ subTitle }}div>
          <div class="extra">
            <slot name="extra">slot>
          div>
        template>
      el-page-header>
      
      <module-form v-if="handleType === 'edit'" v-bind="$attrs" ref="tForm">
        <template v-for="(index, name) in slots" v-slot:[name]="data">
          <slot :name="name" v-bind="data">slot>
        template>
      module-form>
      
      <module-detail v-else v-bind="$attrs">
        <template v-for="(index, name) in slots" v-slot:[name]="data">
          <slot :name="name" v-bind="data">slot>
        template>
      module-detail>
      
      <div
        class="tabs"
        v-if="tabs"
        :style="{ 'margin-top': isTabMargin ? `${tabMarginNum}px` : 0 }"
      >
        <el-tabs
          v-if="tabs && tabs.length > 1"
          v-model="activeName"
          @tab-change="tabsChange"
        >
          <el-tab-pane
            v-for="tab in tabs"
            :key="tab.key"
            :name="tab.key"
            :label="tab.title"
          >
            <slot :name="tab.key">slot>
          el-tab-pane>
        el-tabs>
        <slot v-else :name="tabs && tabs[0].key">slot>
      div>
      <slot name="default">slot>
    div>
    
    <footer class="handle_wrap" v-if="footer !== null">
      <slot name="footer" />
      <div v-if="!slots.footer">
        <el-button @click="back">取消el-button>
        <el-button
          type="primary"
          v-if="handleType === 'edit'"
          @click="saveHandle"
          :loading="loading"
          >{{ btnTxt }}el-button
        >
      div>
    footer>
  div>
template>

<script setup lang="ts" name="TModuleForm">
import { ref, useAttrs, useSlots, nextTick, onMounted } from 'vue'
// import { useRouter } from 'vue-router'
import ModuleDetail from './moduleDetail.vue'
import ModuleForm from './moduleForm.vue'
const props: any = defineProps({
  handleType: {
    type: String,
    default: 'edit', // edit表form表单操作,desc表详情页面
  },
  // 是否使用插槽显示title
  titleSlot: {
    type: Boolean,
    default: false,
  },
  // 是否显示返回箭头
  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,
  },
  // 是否显示底部操作按钮 :footer="null"
  footer: Object,
  title: String,
  subTitle: String,
  tabs: Array as unknown as any[],
  submit: Function,
})
const attrs: any = useAttrs()
const slots = useSlots()
const activeName = ref(props.tabs && props.tabs[0].key)
const loading = ref(false)
// 获取ref
const tForm: any = ref<HTMLElement | null>(null)
// const router = useRouter()

onMounted(() => {
  // console.log('router', router)
  // console.log('onMounted', attrs)
  // console.log('onMounted222', attrs.formOpts)
})
// 抛出事件
const emits = defineEmits(['validateError', 'back', 'tabsChange'])
// 点击保存
const saveHandle = async () => {
  let form = {}
  let formError = {}
  let formOpts = {}
  let successLength = 0
  loading.value = true
  // 过滤非插槽表单
  Object.keys(attrs.formOpts).forEach((key) => {
    if (attrs.formOpts[key].opts) {
      formOpts[key] = attrs.formOpts[key]
    }
  })
  Object.keys(formOpts).forEach(async (formIndex) => {
    const { valid, formData } = await tForm.value
      .getChildRef(formIndex)
      .selfValidate()
    if (valid) {
      successLength = successLength + 1
      form[formIndex] = attrs.formOpts[formIndex].opts.formData
    }
  })
  setTimeout(async () => {
    if (successLength === Object.keys(formOpts).length) {
      // 所有表单都校验成功
      const isSuccess = await props.submit(form)
      if (isSuccess) {
        // 成功
        back()
      }
    } 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]
        }
      })
      emits('validateError', formError)
    }
    loading.value = false
  }, 300)
}
// 点击头部返回或者取消
const back = () => {
  if (props.isShowBack) {
    return
  }
  emits('back')
  if (!props.isGoBackEvent) {
    // router.go(-1)
    history.go(-1)
  }
}
const show = (formType) => {
  nextTick(() => {
    updateFormFields()
    props.formType = formType
  })
}
// 获取默认选中tab
const setSelectedTab = (key) => {
  activeName.value = key
}
// 切换tab
const tabsChange = (tab) => {
  emits('tabsChange', tab)
}
// 清空表单
const resetFormFields = () => {
  let formOpts = {}
  // 过滤非插槽表单
  Object.keys(attrs.formOpts).forEach((key) => {
    if (attrs.formOpts[key].opts) {
      formOpts[key] = attrs.formOpts[key]
    }
  })
  Object.keys(formOpts).forEach((formIndex) => {
    tForm.value.getChildRef(formIndex).resetFields()
  })
}
// 清空校验规则
const clearValidate = () => {
  let formOpts = {}
  // 过滤非插槽表单
  Object.keys(attrs.formOpts).forEach((key) => {
    if (attrs.formOpts[key].opts) {
      formOpts[key] = attrs.formOpts[key]
    }
  })
  Object.keys(formOpts).forEach((formIndex) => {
    tForm.value.getChildRef(formIndex).clearValidate()
  })
}
const updateFormFields = () => {
  let formOpts = {}
  // 过滤非插槽表单
  Object.keys(attrs.formOpts).forEach((key) => {
    if (attrs.formOpts[key].opts) {
      formOpts[key] = attrs.formOpts[key]
    }
  })
  Object.keys(formOpts).forEach((formIndex) => {
    tForm.value.getChildRef(formIndex).updateFields(false)
  })
}
const isShow = (name) => {
  return Object.keys(slots).includes(name)
}
// 暴露方法出去
defineExpose({
  clearValidate,
  resetFormFields,
  updateFormFields,
  setSelectedTab,
  saveHandle,
})
script>
<style lang="scss">
.t_module_form {
  position: relative;
  display: flex;
  flex-grow: 1;
  flex-direction: column;
  height: 100%;
  text-align: left;
  background-color: var(--el-bg-color-page);
  overflow: auto;
  .scroll_wrap {
    display: flex;
    flex-direction: column;
    flex-grow: 1;
    .el-page-header {
      -webkit-box-sizing: border-box;
      box-sizing: border-box;
      margin: 0;
      padding: 0;
      color: var(--el-text-color-primary);
      font-size: 14px;
      font-variant: tabular-nums;
      line-height: 1.5;
      list-style: none;
      -webkit-font-feature-settings: 'tnum';
      font-feature-settings: 'tnum';
      position: relative;
      padding: 16px 24px;
      background-color: var(--el-bg-color);
      .el-page-header__breadcrumb {
        margin: 0;
      }
      .el-page-header__left {
        color: var(--el-text-color-primary);
        align-items: center;
        margin: 0;
        width: 100%;
        .el-icon-back {
          font-weight: bold;
        }
        .el-page-header__title {
          font-size: 18px;
          font-weight: bold;
        }
      }
      .el-page-header__content {
        display: flex;
        align-items: center;
        justify-content: space-between;
        flex: 60%;
        .sub_title {
          flex: 30%;
        }
        .extra {
          flex: 70%;
          display: flex;
          justify-content: flex-end;
        }
      }
    }
    .noContent {
      .el-page-header__left {
        .el-divider {
          display: none;
        }
      }
    }
    // 是否显示返回箭头
    .isShowBack {
      .el-page-header__left {
        .el-page-header__icon {
          display: none;
        }
      }
    }
    .t_form {
      .el-collapse-borderless {
        background-color: var(--el-bg-color);
        .noTitle {
          .el-collapse-header {
            display: none;
          }
        }
        .el-collapse-item {
          background-color: var(--el-bg-color);
          margin-top: 10px;
          border: none;
          &:first-child {
            margin-top: 0;
          }
          .el-collapse-header {
            border-bottom: 1px solid var(--el-border-color);
          }
          .el-collapse-content-box {
            padding: 16px;
          }
        }
      }
    }
    .tabs {
      padding: 0;
      margin: 0;
      .el-tabs {
        .el-tabs__header {
          margin: 0;
          padding: 0 10px;
          background-color: var(--el-bg-color);
        }
        .el-tabs__nav-wrap {
          &::after {
            height: 1px;
          }
        }
      }
    }
  }
  .handle_wrap {
    position: fixed;
    z-index: 4;
    right: 0;
    bottom: 0px;
    height: 60px;
    display: flex;
    align-items: center;
    justify-content: flex-end;
    background-color: var(--el-bg-color);
    border-top: 1px solid var(--el-border-color);
    text-align: right;
    width: 100%;
    .el-button:last-child {
      margin-right: 15px;
    }
  }
}
style>

六、组件地址

gitHub组件地址

gitee码云组件地址

七、相关文章

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


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


vue2/3集成qiankun微前端

你可能感兴趣的:(vue3,form表单组件,element-plus,vue.js,typescript,el-form,vitepress)