Bootstrap

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"
/>
<!-- 第二种详情展示 根据handleType-->
<t-module-form
  title="模块详情组件运用"
  ref="sourceDetail"
  handleType="desc"
  :descData="descData"
/>

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

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

;