2023-09-20 优化模块表单组件;新增文档demo示例
一、最终效果
二、组件集成了以下功能
1、可以多模块配置form表单——配置formOpts对象
2、每个模块可以收起或展开——模块不设置title值取消此功能(或者设置disabled:true)
3、每个模块可以自定义插槽设置
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 ModuleDetail from './moduleDetail.vue'
import ModuleForm from './moduleForm.vue'
const props: any = defineProps({
handleType: {
type: String,
default: 'edit',
},
titleSlot: {
type: Boolean,
default: false,
},
isShowBack: {
type: Boolean,
default: false,
},
isGoBackEvent: {
type: Boolean,
default: false,
},
btnTxt: {
type: String,
default: '保存',
},
isTabMargin: {
type: Boolean,
default: false,
},
tabMarginNum: {
type: Number,
default: 10,
},
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)
const tForm: any = ref<HTMLElement | null>(null)
onMounted(() => {
})
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) {
history.go(-1)
}
}
const show = (formType) => {
nextTick(() => {
updateFormFields()
props.formType = formType
})
}
const setSelectedTab = (key) => {
activeName.value = key
}
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微前端