对el-form
组件的二次封装
我们知道el-form-item
组件需要传入一个基础的属性。
label
: 表示表单每一项的标题。rules
: 表单验证配置prop
: 提供了rules,就需要配置该属性,他的值是每项绑定的v-model
的属性名。style
: 表单样式控制
下面我们写出每项表单的类型约束。
type IFormType = 'input' | 'password' | 'select' | 'datepicker'
export interface IFormItem {
// 来获取对应表单项的数据,我们会将表单全部数据,通过Object传递,所以需要他动态获取。
field: string
// formitem的选项,表单类型
type: IFormType
// 表单项的文本
label: string
// 表单验证规则
rules?: any[]
// 表单的提示文字
placeholder?: any
// 针对select
options?: any[]
// 针对特殊的属性
otherOptions?: any
// 是否显示该表单项
isHidden?: boolean
}
// 整个表单的props对象的约束。
export interface IForm {
// 表单的每一项配置
formItems: IFormItem[]
// label的宽度
labelWidth?: string
// 通过el-row, el-col做响应式
colLayout?: any
// 每个表单项的样式
itemStyle?: any
}
复制代码
我们还提供了头部插槽和尾部插槽,用来实现对头部和尾部的不同定制。他就是对传入的配置项,根据type
字段做判断,来渲染不同的表单控件。
需要传递的props
props: {
// 表单数据绑定
modelValue: {
type: Object,
required: true
},
// 表单渲染的每一项
formItems: {
type: Array as PropType<IFormItem[]>,
default: () => {
return []
}
},
// 表单标题的宽度
labelWidth: {
type: String,
default: '100px'
},
// 每项表单的样式
itemStyle: {
type: Object,
default: () => {
return {
padding: '10px 40px'
}
}
},
// 为表单做布局
colLayout: {
type: Object,
default: () => ({
xl: 6, // >1920px 4个
lg: 8,
md: 12,
sm: 24,
xs: 24
})
}
},
复制代码
组件的封装
<template>
<div class="zh-form">
<div class="form-header">
<slot name="header"></slot>
</div>
<el-form :label-width="labelWidth">
<el-row>
<template v-for="formItem in formItems" :key="formItem.label">
<el-col v-bind="colLayout">
<!-- 通过条件判断来渲染不同的表单 -->
<el-form-item
:label="formItem.label"
:rules="formItem.rules"
:style="itemStyle"
:prop="formItem.field"
v-if="!formItem.isHidden"
>
<!-- 渲染普通input和password -->
<template
v-if="formItem.type === 'input' || formItem.type === 'password'"
>
<el-input
:type="formItem.type"
:placeholder="formItem.placeholder"
v-model="formValues[`${formItem.field}`]"
></el-input>
</template>
<!-- 渲染select表单 -->
<template v-if="formItem.type === 'select'">
<el-select
:placeholder="formItem.placeholder"
v-model="formValues[`${formItem.field}`]"
>
<el-option
v-for="optionItem in formItem.options"
:key="optionItem.title"
:label="optionItem.title"
:value="optionItem.value"
></el-option>
</el-select>
</template>
<!-- 渲染date表单 -->
<template v-if="formItem.type === 'datepicker'">
<el-date-picker
v-bind="formItem.otherOptions"
style="width: 100%"
v-model="formValues[`${formItem.field}`]"
></el-date-picker>
</template>
</el-form-item>
</el-col>
</template>
</el-row>
</el-form>
<div class="form-footer">
<slot name="footer"></slot>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType, ref, watch } from 'vue'
import { IFormItem } from './type'
export default defineComponent({
props: {
modelValue: {
type: Object,
required: true
},
formItems: {
type: Array as PropType<IFormItem[]>,
default: () => {
return []
}
},
labelWidth: {
type: String,
default: '100px'
},
itemStyle: {
type: Object,
default: () => {
return {
padding: '10px 40px'
}
}
},
colLayout: {
type: Object,
default: () => ({
xl: 6, // >1920px 4个
lg: 8,
md: 12,
sm: 24,
xs: 24
})
}
},
setup(props, { emit }) {
// 这里取出表单数据的拷贝,如果修改表单数据的时候,即清空数据,由于这里的代码只能执行一次,所以不能被清除。我们可以使用watch来监听,然后当表单数据清空后,我们就可以重新拷贝了。
const formValues = ref({ ...props.modelValue })
// watch(
// () => props.modelValue,
// (newModelValue) => {
// formValues.value = newModelValue
// }
// )
watch(
formValues,
(newFormValues) => {
emit('update:modelValue', newFormValues)
},
{
deep: true
}
)
return {
formValues
}
}
})
</script>
<style scoped lang="less">
.zh-form {
padding: 20px 10px;
.form-header {
text-align: center;
}
.form-footer {
text-align: right;
}
}
</style>
复制代码
为了使用方便,我们又对封装的Form
组件做了一层抽离。
// page-search.vue
<template>
<div class="page-search">
<zh-form v-bind="formOptions" v-model="formValues">
<template #header>
<h1 class="header">高级检索</h1>
</template>
<template #footer>
<el-button plain size="medium" type="danger" @click="handleClear"
>重置</el-button
>
<el-button plain size="medium" type="primary" @click="handleSearch"
>搜索</el-button
>
</template>
</zh-form>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, PropType } from 'vue'
import ZhForm from '../../../base-ui/Form'
import { IForm } from '../../../base-ui/Form/src/type'
export default defineComponent({
props: {
formOptions: {
type: Object as PropType<IForm>,
required: true
}
},
components: {
ZhForm
},
emits: ['handleClear', 'handleSearch'],
setup(props, { emit }) {
const originFormValues: any = {}
props.formOptions.formItems.forEach((item) => {
originFormValues[`${item.field}`] = ''
})
// 定义表单相关的数据。
const formValues = ref(originFormValues)
// 处理清空按钮
const handleClear = () => {
// formValues.value = originFormValues
for (const key in originFormValues) {
formValues.value[`${key}`] = originFormValues[key]
}
// 将事件传入父组件
emit('handleClear')
}
// 处理搜索
const handleSearch = () => {
emit('handleSearch', formValues.value)
}
return {
formValues,
handleClear,
handleSearch
}
}
})
</script>
复制代码
下面我们就可以传入一个配置文件,就渲染出来一个表单了。
我们来举个例子吧。
// 配置文件ts
import { IForm, IFormItem } from '@/base-ui/Form/src/type'
const formItemOptions: IFormItem[] = [
{
field: 'name',
label: '角色名',
type: 'input',
placeholder: '请输入角色名',
rules: [
{
required: true,
message: 'Please input Activity name',
trigger: 'blur'
},
{
min: 3,
max: 5,
message: 'Length should be 3 to 5',
trigger: 'blur'
}
]
},
{
field: 'time',
label: '根据时间选择内容',
type: 'datepicker',
otherOptions: {
startPlaceholder: '开始时间',
endPlaceholder: '结束时间',
type: 'daterange'
}
}
]
const formOptions: IForm = {
formItems: formItemOptions,
labelWidth: '120px',
colLayout: {
xl: 6, // >1920px 4个
lg: 8,
md: 12,
sm: 24,
xs: 24
}
// itemStyle: { padding: '20px 40px' }
}
export default formOptions
复制代码
<template>
<div class="test">
<page-search :formOptions="formOptions"></page-search>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import formOptions from './config/formConfig'
import { PageSearch } from '../../../../components/PageSearch'
export default defineComponent({
components: {
PageSearch
},
setup() {
return {
formOptions
}
}
})
</script>
复制代码
下面就可以配制出自定义的表单组建了,是不是很方便。下面就来看看效果吧。
其实上面组件还存在一个小问题,就是做表单双向绑定的时候,不好理解。我们还有一种方法,就是自己实现v-model
, 就是我们在使用element-plus表单组件的时候,不通过v-model绑定数据,我们自己通过modelValue
和事件update:modelValue($event, formItem.field)
实现 这样就比较容易理解。下面来看看吧。
<el-input
:type="formItem.type"
:placeholder="formItem.placeholder"
:modelValue="modelValue[`${formItem.field}`]"
@update:modelValue="handleValueChange($event, formItem.field)"
></el-input>
const handleValueChange = (value: any, field: string) {
emit("update:modelValue", {...props.modelValue, [field]: value})
}
复制代码
对el-table组件的二次封装
这个表格组件封装需要考虑很多东西,比较复杂。他主要是对插槽的处理。因为我们每次渲染的时候,每列的内容可能都不一样,所以他主要是提供插槽,然后外界实现。
对于el-table
组件他只需要绑定列表数据即可。
对于el-table-column
它需要提供一个属性。
prop
:对应数据项的字段。如果不提供将不会渲染数据。label
: 提供每一列的标题。
我们还需要对一个非数据项做处理。
- 选中列
- 编号列
- 操作列
前两项可以通过el-table-column内置的属性完成,最后一项,我们可以通过传入配置项完成。
下面来看看table组件的props吧。
props: {
// 数据列表
tableData: {
type: Array,
required: true
},
// el-table-column配置项
propList: {
type: Array,
required: true
},
// 是否具有编号列
isNumberColumn: {
type: Boolean,
default: false
},
// 是否具有选中列
isSelectColumn: {
type: Boolean,
default: false
},
// 列表标题
listTitle: {
type: String,
required: true
},
// 数据条数
tableDataCount: {
type: Number,
default: 0
},
// 分页参数
page: {
type: Object,
default: () => ({ currentPage: 0, pageSize: 10 })
}
复制代码
组件封装
<template>
<div class="contain-table">
<!-- 列表头部组件插槽 -->
<div class="list-header">
<slot name="header">
<!-- 提供默认的插槽内容 -->
<div class="list-title">{{ listTitle }}</div>
<div class="list-operate">
<slot name="listOperate"></slot>
</div>
</slot>
</div>
<el-table :data="tableData" border style="width: 100%">
<!-- 对选中列进行封装 -->
<el-table-column
v-if="isSelectColumn"
type="selection"
width="60"
align="center"
></el-table-column>
<!-- 对于编号栏是否存在进行封装 -->
<el-table-column
v-if="isNumberColumn"
type="index"
label="编号"
width="80"
align="center"
></el-table-column>
<!-- 数据列表项的渲染,提供插槽。 -->
<template v-for="prop in propList" :key="prop.prop">
<el-table-column v-bind="prop" align="center" show-overflow-tooltip>
<template #default="scope">
<slot :name="prop.slotName" :row="scope.row">
{{ scope.row[prop.prop] }}
</slot>
</template>
</el-table-column>
</template>
</el-table>
<!-- 列表尾部组件插槽 , 默认是分页器-->
<div class="list-footer">
<slot name="listFooter">
<el-pagination
:currentPage="page.currentPage"
:page-size="page.pageSize"
layout="total, prev, pager, next"
:total="tableDataCount"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
</el-pagination>
</slot>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
props: {
tableData: {
type: Array,
required: true
},
propList: {
type: Array,
required: true
},
// 是否具有编号列
isNumberColumn: {
type: Boolean,
default: false
},
// 是否具有选中列
isSelectColumn: {
type: Boolean,
default: false
},
// 列表标题
listTitle: {
type: String,
required: true
},
tableDataCount: {
type: Number,
default: 0
},
// 分页参数
page: {
type: Object,
default: () => ({ currentPage: 0, pageSize: 10 })
}
},
emits: ['update:page'],
setup(props, { emit }) {
// 处理分页器
const handleCurrentChange = (currentPage: number) => {
emit('update:page', { ...props.page, currentPage })
}
const handleSizeChange = (pageSize: number) => {
emit('update:page', { ...props.page, pageSize })
}
return {
handleCurrentChange,
handleSizeChange
}
}
})
</script>
<style scoped lang="less">
.list-header {
display: flex;
height: 45px;
padding: 0 10px;
justify-content: space-between;
align-items: center;
.list-title {
font-size: 20px;
font-weight: 700;
}
.list-operate {
align-items: center;
}
}
.list-footer {
display: flex;
justify-content: flex-end;
width: 100%;
margin-top: 20px;
}
</style>
复制代码
为了使用方便,我们又对封装的Table
组件做了一层抽离。其实这一层就是实现相同的内容,不同的内容到页面组件中自己实现。
-
这里主要实现的是共同的插槽内容,比如状态栏(都是用el-tag实现)。
-
如果需要提供不同的插槽内容(例如展示图片,我们不可能都在这一层组件实现,这样实现的插槽太多,而且这只是个别组件才会实现的,这时候就需要在页面组件中自己实现了。),我们还需要提供插槽,这里就涉及到了跨组件插槽的传递了。
-
我们需要根据传入的propList中的slotName过滤,然后来提供插槽。
<template>
<div class="page-contain-table">
<zh-contain-table
:tableData="tableData"
v-bind="containTableConfig"
:tableDataCount="tableDataCount"
v-model:page="pageInfo"
>
<!-- 列表头部插槽的实现 -->
<template #listOperate>
<el-button
type="primary"
size="medium"
plain
@click="handleAddListItem"
>{{ tableOperateTitle }}</el-button
>
</template>
<!-- 状态插槽 -->
<template #status="scope">
<el-tag type="success" v-if="scope.row.status === 1">启用</el-tag>
<el-tag type="info" v-else>禁用</el-tag>
</template>
<!-- 传入操作列插槽内容 -->
<template #operateColumn="scope">
<el-button
plain
size="mini"
type="primary"
@click="handleEditModalDialog(scope.row)"
>编辑</el-button
>
<el-button plain size="mini" type="danger">删除</el-button>
</template>
<!-- 提供插槽,为了实现个性化配置 -->
<template
v-for="item in otherPropSlots"
:key="item.prop"
#[item.slotName]="scope"
>
<template v-if="item.slotName">
<slot :name="item.slotName" :row="scope.row"></slot>
</template>
</template>
</zh-contain-table>
</div>
</template>
<script lang="ts">
import { defineComponent, computed, ref, watch } from 'vue'
import ZhContainTable from '../../../base-ui/ContainTable'
import { useStore } from '../../../store'
export default defineComponent({
props: {
containTableConfig: {
type: Object,
required: true
},
// 用于请求每个菜单的数据,所以传入一个pageName
pageName: {
type: String,
required: true
},
tableOperateTitle: {
type: String,
default: '添加用户'
}
},
emits: ['handleAddListItem', 'editListItem'],
components: {
ZhContainTable
},
setup(props, { emit }) {
const store = useStore()
const pageInfo = ref({
pageSize: 5,
currentPage: 1
})
// 当点击分页按钮时,重新请求数据
watch(pageInfo, () => {
getListData()
})
const getListData = (queryInfo: any = {}) => {
store.dispatch('system/getPageListAction', {
pageName: props.pageName,
queryInfo: {
offset: pageInfo.value.currentPage * pageInfo.value.pageSize,
size: pageInfo.value.pageSize,
...queryInfo
}
})
}
getListData()
// 获取列表数据
const tableData = computed(() =>
store.getters[`system/getPageListData`](props.pageName)
)
// 获取列表条数
const tableDataCount = computed(() =>
store.getters[`system/getPageListDataCount`](props.pageName)
)
// 过滤slotName
const otherPropSlots = props.containTableConfig?.propList.filter(
(item: any) => {
if (item.slotName === 'status') return false
if (item.slotName === 'createAt') return false
if (item.slotName === 'updateAt') return false
if (item.slotName === 'operateColumn') return false
return true
}
)
// 当点击添加列表项时
const handleAddListItem = () => {
emit('handleAddListItem')
}
// 当点击编辑按钮的时
const handleEditModalDialog = (item: any) => {
emit('editListItem', item)
}
return {
tableData,
tableDataCount,
getListData,
pageInfo,
otherPropSlots,
handleAddListItem,
handleEditModalDialog
}
}
})
</script>
<style scoped lang="less">
.page-contain-table {
padding: 20px;
border-top: 20px solid #f5f5f5;
}
</style>
复制代码
下面我们就来传入配置文件,显然出一个表格列表吧。
// 定义需要展示哪些数据
const propList = [
{
prop: 'name',
label: '角色',
slotName: 'name'
},
{
prop: 'intro',
label: '具有的权限',
slotName: 'intro'
},
{
prop: 'createAt',
label: '创建时间',
slotName: 'createAt'
},
{
prop: 'updateAt',
label: '更新时间',
slotName: 'updateAt'
},
// 操作列
{
label: '操作',
slotName: 'operateColumn',
width: '150'
}
]
const containTableConfig = {
listTitle: '角色列表',
propList,
isNumberColumn: true,
isSelectColumn: true
}
export default containTableConfig
复制代码
页面组件,渲染页面
<template>
<div class="test">
<page-search :formOptions="formOptions"></page-search>
<page-contain-table
:containTableConfig="containTableConfig"
pageName="role"
></page-contain-table>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import formOptions from './config/formConfig'
import containTableConfig from './config/containTableConfig'
import { PageSearch } from '../../../../components/PageSearch'
import { PageContainTable } from '../../../../components/PageContainTable'
export default defineComponent({
components: {
PageSearch,
PageContainTable
},
setup() {
return {
formOptions,
containTableConfig
}
}
})
</script>
复制代码
下面再举一个在页面中传入插槽的定制的页面。
// 定义需要展示哪些数据
const propList = [
{ prop: 'name', label: '商品名称', minWidth: '80' },
{ prop: 'oldPrice', label: '原价格', minWidth: '80', slotName: 'oldPrice' },
{ prop: 'newPrice', label: '现价格', minWidth: '80', slotName: 'newPrice' },
{ prop: 'imgUrl', label: '商品图片', minWidth: '100', slotName: 'image' },
{ prop: 'status', label: '状态', minWidth: '100', slotName: 'status' },
{
prop: 'createAt',
label: '创建时间',
minWidth: '250',
slotName: 'createAt'
},
{
prop: 'updateAt',
label: '更新时间',
minWidth: '250',
slotName: 'updateAt'
},
// 操作列
{
label: '操作',
slotName: 'operateColumn',
width: '150'
}
]
const containTableConfig = {
listTitle: '商品列表',
propList,
isNumberColumn: true,
isSelectColumn: true
}
export default containTableConfig
复制代码
页面组件
<template>
<div class="goods">
<page-search :formOptions="formOptions"></page-search>
<page-contain-table
:containTableConfig="containTableConfig"
pageName="goods"
>
<!-- 对图片的插槽处理 -->
<template #image="scope">
<el-image
style="width: 60px; height: 60px"
:src="scope.row.imgUrl"
:preview-src-list="[scope.row.imgUrl]"
hide-on-click-modal
>
</el-image>
</template>
<!-- 对价格进行处理 -->
<template #oldPrice="scope"> ¥{{ scope.row.oldPrice }} </template>
<template #newPrice="scope"> ¥{{ scope.row.newPrice }} </template>
</page-contain-table>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import formOptions from './config/formConfig'
import containTableConfig from './config/containTableConfig'
import { PageSearch } from '../../../../components/PageSearch'
import { PageContainTable } from '../../../../components/PageContainTable'
export default defineComponent({
name: 'goods',
components: {
PageSearch,
PageContainTable
},
setup() {
return {
formOptions,
containTableConfig
}
}
})
</script>
复制代码