一、最终效果
二、组件集成了以下功能
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、代码示例
<t-module-form
title="基本使用"
ref="sourceForm"
:formOpts="formOpts"
:submit="submit"
/>
<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源码
<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'
},
isShowBack: {
type: Boolean,
default: false
},
isGoBackEvent: {
type: Boolean,
default: false
},
btnTxt: {
type: String,
default: '保存'
},
isTabMargin: {
type: Boolean,
default: false
},
tabMarginNum: {
type: Number,
default: 10
},
footerBtn: Object,
title: String,
subTitle: String,
tabs: Array,
getContainer: Function,
submit: Function
},
data() {
return {
activeName: this.tabs && this.tabs[0].key,
loading: false
}
},
methods: {
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] = 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源码
<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源码
<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再次封装基础组件文档