对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>
网络异常,图片无法展示
|