效果
- 包含了表单搜索、表格、分页
- 可以修改表格slots,不影响el-table原始的方法、属性
- 表格上面可以添加按钮
代码
表格列配置
export const columns = [
{ type: 'index', label: '序号', width: 100, align: 'center' },
{
type: 'selection',
width: 55
},
{
prop: 'name',
label: '模型名称',
width: 200
},
{
prop: 'identification',
label: '创建方式'
},
{
prop: 'topModel',
label: '故障模式',
width: 200
},
{
prop: 'createDate',
label: '最后更新时间',
width: 200
},
{
prop: 'user',
label: '修改人',
width: 120
},
{
prop: 'publishStatus',
label: '模型状态'
},
{
label: '操作',
fixed: 'right',
width: 200,
slots: 'operator'
}
]
列的类型,可以通过children生成多级表头
表单配置
时间范围的输入框,v-model绑定的是一个数组,propArray代表搜索的时候接口传入的参数
表格的属性方法
在我们的自定义组件上可以直接传el-table原生的属性和事件
表格数据获取
getListApi传入接口,defaultParams是接口默认的参数
useCustomTable方法有两个部分,一个是获取表格接口,然后导出了表格数据,分页,loading等信息
还有就是生成表格的DOM对象,表单分页的操作在useTableDom里,然后传入了tableObject,分页修改时,tableObject的分页信息变化,useTableApi就会重新获取接口
表单数据传入的是useTableApi的响应式数据
使用createVNode的方式导出el-table, 在父组件就可以直接给Table传属性和获取Table的事件
多级表头通过递归来实现
组件的全部代码
目录结构
export interface ColumnsType {
type?: 'selection' | 'expand' | 'index' | string
prop?: string
label?: string | '操作'
width?: number
minWidth?: number
visible?: boolean
fixed?: 'left' | 'right' | string
align?: 'left' | 'right' | 'center' | string
children?: ColumnsType[]
slots?: string
}
interface UseCustomTableConfig {
api: ApiConfig
columns: ColumnsType[]
form?: FormConfig
slots?: {
[key: string]: (arg: any) => VNode
}
toolbar?: () => VNode
}
interface ApiConfig {
getListApi: (option: any) => Promise<any>
defaultParams?: any
}
import { FormConfig } from './useForm'
import { useTableApi } from './useTableApi'
import { useTableDom } from './useTableDom'
import { onMounted, VNode } from 'vue'
export const useCustomTable = (config: UseCustomTableConfig) => {
const { getListApi, defaultParams } = config.api
const { columns, form, slots, toolbar } = config
const { getList, tableObject } = useTableApi({
getListApi: getListApi,
defaultParams: defaultParams
})
onMounted(getList)
const { Table } = useTableDom({
tableObject: tableObject,
columns: columns,
getList: getList,
form: form,
slots: slots,
toolbar: toolbar
})
return {
getList,
tableObject,
Table
}
}
/*
* @Author: zengzhaoyan
* @Date: 2023-12-13 11:09:13
* @LastEditors: zengzhaoyan
* @LastEditTime: 2023-12-15 16:36:22
* @Description: 表单组件
* @FilePath: /zzy/src/hooks/useCustomTable/useForm.ts
*/
import {
ElForm,
ElFormItem,
ElSwitch,
ElInput,
ElInputNumber,
ElCheckbox,
ElCheckboxGroup,
ElRadioGroup,
ElRadio,
ElSelect,
ElOption,
ElDatePicker
} from 'element-plus'
import { createVNode, reactive, ref, VNode, Ref } from 'vue'
export interface ObjectForm {
loading?: boolean
type?:
| 'switch'
| 'textarea'
| 'number'
| ''
| 'text'
| 'checkbox'
| 'radio'
| 'multipleselect'
| 'select'
| 'datetime'
| 'daterange'
| 'upload'
| 'group'
| 'password'
| string
rule?: rule[]
hidden?: boolean
prop: string
propArray?: [string, string] | undefined // 代表v-model绑定一个数组
label: string
disabled?: boolean
strictly?: number
multiple?: boolean
stepStrictly?: number
placeholder?: string
width?: string
clearable?: boolean
filterable?: boolean
defaultProp?: { label: string; value: string }
maxlength?: number
options?: Array<{ label: string; value: string | number }>
valueFormat?: string // 时间值的格式化
format?: string // 时间显示的格式化
remoteMethod?: (value: string, callback: (list: any[]) => void) => any
disabledDate?: (time: any) => boolean
apiFun?: (params: any) => Promise<any>
params?: any
slots?: (arg: any) => VNode
}
export type rule = {
validator?: any
required?: boolean
message?: string
pattern?: RegExp
trigger?: 'blur' | 'change' | string
}
type RulesType = Record<string, Array<rule>>
export interface FormConfig {
itemWidth?: string
labelWidth?: string
inline?: boolean
labelPosition?: string | 'top' | 'left' | 'right'
disabledAll?: boolean
rules?: RulesType
item: ObjectForm[]
}
interface FormProps {
disabledAll?: boolean
labelWidth?: string
labelPosition?: string
itemWidth?: string
inline?: boolean
}
export const useForm = (
formConfig: FormConfig | undefined,
formData: { [key: string]: any },
append?: () => VNode
) => {
const formValue =
reactive(JSON.parse(JSON.stringify(formData))) || reactive({})
const rules = (formConfig && formConfig.rules) || {}
const setRules = (item: ObjectForm) => {
if (item.rule && !item.hidden) {
rules[item.prop] = item.rule
}
}
const initDefault = async () => {
if (!formConfig || !formConfig.item.length) return
for (let i = 0; i < formConfig.item.length; i++) {
const item = formConfig.item[i]
if (item.apiFun) {
item.loading = true
const res = await item.apiFun(item.params)
item.loading = false
item.options = res.data
}
setRules(item)
}
}
initDefault()
const formRef = ref()
const resetFields = () => {
formRef.value.resetFields()
}
const getValue = async () => {
if (Object.keys(rules).length > 0) {
const res = await formRef.value.validate()
if (res) {
return formValue
}
return null
}
return formValue
}
const props: Ref<FormProps> = ref({})
const MyForm = (propss: any, context: any) => {
if (!formConfig || !formConfig.item.length) return createVNode('')
props.value = propss
return createVNode(
ElForm,
{
ref: formRef,
...context.attrs,
model: formValue,
rules: rules,
inline: true,
...createFormAttrs()
},
[createItem(), appendDom()]
)
}
const createFormAttrs = () => {
if (!formConfig || !formConfig.item.length) return createVNode('')
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { item, rules, ...rest } = formConfig
return rest
}
const createItem = () => {
if (!formConfig || !formConfig.item.length) return
return formConfig.item.map((item: ObjectForm) => {
return createVNode(
ElFormItem,
{
label: item.label,
prop: item.prop,
rules: item.rule
},
[createItemInput(item)]
)
})
}
const appendDom = () => {
return createVNode(ElFormItem, {}, [append && append()])
}
const createItemInput = (item: ObjectForm) => {
if (!formConfig || !formConfig.item.length) return
if (item.slots) {
return item.slots(item)
}
const defaultAttrs = {
modelValue: formValue[item.prop],
'onUpdate:modelValue': (newValue: any) => {
formValue[item.prop] = newValue
if (item.propArray) {
formValue[item.propArray[0]] = newValue[0]
formValue[item.propArray[1]] = newValue[1]
}
},
disabled: item.disabled,
style: {
width: item.width ? item.width : formConfig.itemWidth
}
}
const isCheckboxOrRadio = item.type
? ['checkbox', 'radio'].includes(item.type)
: false
if (item.type === 'switch') {
return createVNode(ElSwitch, {
...defaultAttrs
})
} else if (item.type === 'textarea') {
return createVNode(ElInput, {
type: 'textarea',
...defaultAttrs
})
} else if (item.type === 'number') {
return createVNode(ElInputNumber, {
...defaultAttrs,
'controls-position': 'right',
'step-strictly': item.strictly
})
} else if (isCheckboxOrRadio) {
const options = item.options?.map((option: any) =>
createVNode(
item.type === 'checkbox' ? ElCheckbox : ElRadio,
{ label: option.value },
[option.label]
)
)
const Component =
item.type === 'checkbox' ? ElCheckboxGroup : ElRadioGroup
return createVNode(Component, { ...defaultAttrs }, options)
} else if (item.type === 'text' || !item.type || item.type === 'password') {
return createVNode(ElInput, {
...defaultAttrs,
placeholder: item.placeholder
? item.placeholder
: `请输入${item.label}`,
maxlength: item.maxlength ? item.maxlength : null,
type: item.type === 'password' ? 'password' : 'text',
'auto-complete': 'off',
'show-word-limit': true
})
} else if (item.type === 'select') {
return createVNode(
ElSelect,
{
...defaultAttrs,
clearable: item.clearable || true,
multiple: item.multiple,
filterable: item.filterable,
'default-first-option': true,
'v-loading': item.loading,
remoteMethod: async (value: any) => {
if (item.remoteMethod) {
item.loading = true
item.remoteMethod(value, (list) => {
item.options = reactive(list)
item.loading = false
})
}
}
},
item.options?.map((option: any, index: number) => {
return createVNode(ElOption, {
key: index,
label: item.defaultProp
? option[item.defaultProp.label]
: option.label,
value: item.defaultProp
? option[item.defaultProp.value]
: option.value,
disabled: option.disabled ? option.disabled : false
})
})
)
} else if (item.type === 'datetime') {
return createVNode(ElDatePicker, {
...defaultAttrs,
format: item.format || 'YYYY-MM-DD HH:mm:ss',
'value-format': item.valueFormat,
placeholder: '选择日期时间',
'disabled-date': item.disabledDate
})
} else if (item.type === 'daterange') {
return createVNode(ElDatePicker, {
type: 'daterange',
...defaultAttrs,
format: item.format || 'YYYY-MM-DD HH:mm:ss',
'value-format': item.valueFormat,
'range-separator': '至',
'start-placeholder': '开始日期',
'end-placeholder': '结束日期'
})
}
}
return {
MyForm,
getValue,
resetFields,
formRef
}
}
/*
* @Author: zengzhaoyan
* @Date: 2023-12-15 09:55:28
* @LastEditors: zengzhaoyan
* @LastEditTime: 2023-12-15 16:31:26
* @Description:
* @FilePath: /zzy/src/hooks/useCustomTable/usePagination.tsx
*/
import { createVNode } from 'vue'
import { ElPagination } from 'element-plus'
import { TableObject } from './useTableApi'
export const usePagination = (tableObject: TableObject) => {
const pagination = () => {
return createVNode(ElPagination, {
vShow: tableObject.total > 0,
style: 'margin: 10px 10px 0 0; flex: 0 0 auto; align-self: flex-end;',
background: true,
layout: 'total, sizes, prev, pager, next, jumper',
'v-model:currentPage': tableObject.page,
'v-model:pageSize': tableObject.limit,
total: tableObject.total,
'page-sizes': [10, 20, 30, 50, 100],
onSizeChange: handleSizeChange,
onCurrentChange: handleCurrentChange
})
}
const handleSizeChange = (val: number) => {
tableObject.limit = val
tableObject.page = 1
}
const handleCurrentChange = (val: number) => {
tableObject.page = val
}
return {
pagination
}
}
/*
* @Author: zengzhaoyan
* @Date: 2023-12-15 09:26:01
* @LastEditors: zengzhaoyan
* @LastEditTime: 2023-12-15 17:04:07
* @Description:
* @FilePath: /zzy/src/hooks/useCustomTable/useTableApi.ts
*/
import { reactive, unref, computed, watch } from 'vue'
interface UseTableConfig<T> {
getListApi: (option: any) => Promise<T>
defaultParams?: any
}
export interface TableObject<T = any> {
limit: number
page: number
total: number
tableList: T[]
params: any
loading: boolean
exportLoading: boolean
currentRow: any
}
interface ResponseType<T = any> {
data: T[]
statusCode: number
total: number
page: {
dataCount: number
}
}
export const useTableApi = <T = any>(config?: UseTableConfig<T>) => {
const tableObject = reactive<TableObject<T>>({
// 页数
limit: 10,
// 当前页
page: 1,
// 总条数
total: 10,
// 表格数据
tableList: [],
// AxiosConfig 配置
params: {
...(config?.defaultParams || {})
},
// 加载中
loading: true,
// 导出加载中
exportLoading: false,
// 当前行的数据
currentRow: null
})
const paramsObj = computed(() => {
return {
...tableObject.params,
limit: tableObject.limit,
page: tableObject.page
}
})
const methods = {
getList: async () => {
tableObject.loading = true
const res = await config?.getListApi(unref(paramsObj)).finally(() => {
tableObject.loading = false
})
console.log(res)
if (res) {
tableObject.tableList = (res as unknown as ResponseType).data
if ((res as unknown as ResponseType).page.dataCount) {
tableObject.total = (res as unknown as ResponseType).page.dataCount
}
}
}
}
watch(
() => tableObject.page,
() => {
methods.getList()
}
)
watch(
() => tableObject.limit,
() => {
// 当前页不为1时,修改页数后会导致多次调用getList方法
if (tableObject.page === 1) {
methods.getList()
} else {
tableObject.page = 1
methods.getList()
}
}
)
return {
methods,
getList: methods.getList,
tableList: tableObject.tableList,
tableObject
}
}
/*
* @Author: zengzhaoyan
* @Date: 2023-12-15 09:38:07
* @LastEditors: zengzhaoyan
* @LastEditTime: 2023-12-15 16:39:14
* @Description:
* @FilePath: /zzy/src/hooks/useCustomTable/useTableDom.tsx
*/
import { ColumnsType } from './index'
import { usePagination } from './usePagination'
import { useForm } from './useForm'
import { createVNode, VNode } from 'vue'
import { ElTable } from 'element-plus'
import { TableObject } from './useTableApi'
import { FormConfig } from './useForm'
interface TableConfig {
tableObject: TableObject
columns: ColumnsType[]
getList: () => Promise<any>
form?: FormConfig | undefined
slots?: {
[key: string]: (arg: any) => VNode
}
toolbar?: () => VNode
}
export const useTableDom = (config: TableConfig) => {
const { tableObject, columns, getList, form, slots, toolbar } = config
const append = () => {
return (
<>
<el-button onClick={Cancel}>重置</el-button>
<el-button type="primary" onClick={onSubmit}>
确认
</el-button>
</>
)
}
const onSubmit = () => {
const searchValue: Promise<Record<string, any>> = getValue()
searchValue.then((res) => {
Object.assign(tableObject.params, res)
getList()
})
}
const Cancel = () => {
resetFields()
tableObject.params = {}
getList()
}
const { MyForm, getValue, resetFields } = useForm(
form,
tableObject.params,
append
)
const { pagination } = usePagination(tableObject)
const renderTableColumn = (column: ColumnsType) => (
<el-table-column
{...createTableColumnConfig(column)}
v-slots={{
default: (scope: any) => {
if (column.slots && slots) {
return slots[column.slots](scope)
}
}
}}
>
{column.children && column.children.map(renderTableColumn)}
</el-table-column>
)
// el-table-column属性
const createTableColumnConfig = (column: ColumnsType) => {
return {
type: column.type,
prop: column.prop,
label: column.label,
width: column.width,
minWidth: column.minWidth,
fixed: column.fixed,
align: column.align || 'center'
}
}
const Table = (props: Record<string, any>) => {
return createVNode(
'div',
{
style: {
height: '100%',
'overflow-y': 'auto',
display: 'flex',
'flex-direction': 'column'
}
},
[
createVNode(MyForm, { style: { flex: '0 0 auto' } }),
createToolBar(),
createVNode(
ElTable,
{
style: {
flex: 1,
height: '100%',
'overflow-y': 'auto'
},
data: tableObject.tableList,
'highlight-current-row': true,
border: true,
stripe: true,
...props,
'v-loading': tableObject.loading
},
columns.map(renderTableColumn)
),
pagination()
]
)
}
const createToolBar = () => {
if (toolbar) {
return createVNode(
'div',
{ style: 'flex: 0 0 auto;align-self: flex-end;padding-bottom:10px' },
[toolbar()]
)
}
}
return {
Table
}
}
如何使用
可以将列配置和表单配置单独提取出来也可以写在一个文件中
import { defineComponent } from 'vue'
import { useCustomTable } from '@/hooks/useCustomTable'
import { listByPage } from '@/api/faultTree'
import { columns } from './columns.config'
import { formConfig } from './form.config'
export default defineComponent({
setup() {
const api = {
getListApi: listByPage,
defaultParams: {
publishStatus: 0
}
}
const toolbar = () => {
return <el-button>导出</el-button>
}
const slots = {
operator: () => {
return <el-button>编辑</el-button>
}
}
const { Table } = useCustomTable({
api: api,
columns: columns,
form: formConfig,
toolbar: toolbar,
slots: slots
})
const onSelectionChange = (selection: any) => {
console.log(selection)
}
return () => {
return (
<div style="padding: 20px 40px">
<Table border={false} onSelectionChange={onSelectionChange} />
</div>
)
}
}
})
export const columns = [
{ type: 'index', label: '序号', width: 100, align: 'center' },
{
type: 'selection',
width: 55
},
{
prop: 'name',
label: '模型名称',
width: 200
},
{
prop: 'identification',
label: '创建方式'
},
{
prop: 'topModel',
label: '故障模式',
width: 200
},
{
prop: 'createDate',
label: '最后更新时间',
width: 200
},
{
prop: 'user',
label: '修改人',
width: 120
},
{
prop: 'publishStatus',
label: '模型状态'
},
{
label: '操作',
fixed: 'right',
width: 200,
slots: 'operator'
}
]
import { reactive } from 'vue'
export const formConfig = reactive({
labelWidth: '120px',
itemWidth: '200px',
labelPosition: 'right',
item: [
{
label: '模型名称',
prop: 'name',
type: 'text'
},
{
label: '故障模式',
prop: 'topName',
type: 'text'
},
{
label: '最后更新时间',
prop: 'date',
propArray: ['beginUpdateTime', 'endUpdateTime'] as [string, string],
type: 'daterange',
format: 'YYYY-MM-DD',
valueFormat: 'YYYY-MM-DD HH:mm:ss'
}
]
})