高级表格组件
说明:el-table的二次封装,实现自适应高度、列和行拖拽。
1.实现效果
2.封装的表格VTable.vue
<template>
<el-table
ref="tableRef"
v-bind="props"
v-on="events"
:data="localData"
v-loading="loading"
style="width: 100%"
>
<v-table-column
v-for="(column, index) in localCols"
:key="column.id || index"
v-bind="setColumnDefault(column)"
></v-table-column>
<!-- 设置el-table默认插槽 -->
<slot></slot>
<!-- 设置el-table的empty和append插槽 -->
<template #empty>
<slot name="empty"></slot>
</template>
<template #append>
<slot name="append"></slot>
</template>
</el-table>
<slot name="footer">
<div :class="['flex p-2', paginationClass]" v-if="isDefined(pagination)">
<el-pagination v-bind="pagination" v-on="pageEvents">
<template #default="scope" v-if="pagination.defaultSlot">
<component v-bind="scope" :is="pagination.defaultSlot"></component>
</template>
</el-pagination>
</div>
</slot>
</template>
<script lang="tsx" setup>
import { nextTick } from 'vue'
import { isDefined } from '@vueuse/core'
import type { TableColumnType, VTableEmitsType, VTableProps } from './types'
import VTableColumn from './VTableColumn.vue'
import { exposeEventsUtils, forwardEventsUtils } from '@/utils/format'
import Sortable from 'sortablejs'
import DragIcon from './DragIcon.vue'
// 接收传入的数据
const props = withDefaults(defineProps<VTableProps>(), {
pagination: () => ({
align: 'right',
size: 'default',
small: false,
background: false,
layout: 'total, sizes, prev, pager, next, jumper',
pageSizes: [10, 20, 30, 40, 50, 100],
total: 0
}),
stripe: false,
border: true,
size: 'default',
fit: true,
showHeader: true,
highlightCurrentRow: false,
emptyText: 'No data',
defaultExpandAll: false,
tooltipEffect: 'dark',
showSummary: false,
flexible: false,
selectOnIndeterminate: true,
indent: 16,
tableLayout: 'fixed',
scrollbarAlwaysOn: false,
// 自适应高度
adaptive: false,
loading: false,
draggableCol: false,
// 是否可以拖拽行
draggableRow: false,
// rowKey: 'id'
})
const tableRef = ref()
// 传出el-table事件,在封装的组件上直接使用即可
const emits = defineEmits<VTableEmitsType>()
// el-table所有事件名称
const eventsName = [
'select',
'select-all',
'selection-change',
'cell-mouse-enter',
'cell-mouse-leave',
'cell-contextmenu',
'cell-click',
'cell-dblclick',
'row-click',
'row-contextmenu',
'row-dblclick',
'header-click',
'header-contextmenu',
'sort-change',
'filter-change',
'current-change',
'header-dragend',
'expand-change'
]
// el-pagination分页事件名称
const pageEventsName = ['size-change', 'current-change', 'prev-click', 'next-click']
// el-table所有方法名称,可以在封装的组件上直接使用
const exposeEvents = [
'clearSelection',
'getSelectionRows',
'toggleRowSelection',
'toggleAllSelection',
'toggleRowExpansion',
'setCurrentRow',
'clearSort',
'clearFilter',
'doLayout',
'sort',
'scrollTo',
'setScrollTop',
'setScrollLeft'
]
// 获取所有事件集合为events对象
const events = forwardEventsUtils(emits, eventsName)
// 获取所有分页事件集合为pageEvents对象
const pageEvents = forwardEventsUtils(emits, pageEventsName, 'page-')
// 暴露table所有方法,可以在封装的组件中使用ref直接使用
const expose = exposeEventsUtils(tableRef, exposeEvents)
// 定义本地columns,用于列和行的拖拽
const localCols = ref(props.columns as TableColumnType[])
// 定义本地data,用于列和行的拖拽
const localData = ref(props.data as any[])
const rowKey = ref(props.rowKey as string)
defineExpose({ ...expose })
const paginationClass = computed(() => {
let defaultsClass = 'justify-center'
if (props.pagination && props.pagination.align) {
if (props.pagination.align === 'left') {
defaultsClass = 'justify-start'
}
if (props.pagination.align === 'right') {
defaultsClass = 'justify-end'
}
}
return defaultsClass
})
// 设置table-columns默认的属性值
const columnDefaults = {
sortable: false,
'sort-orders': ['ascending', 'descending', null],
resizable: true,
align: 'left',
'reserve-selection': false,
'filter-multiple': true
}
onBeforeMount(() => {
localCols.value = addId(props.draggableCol, props.columns)
localData.value = addId(props.draggableRow, props.data)
if (props.draggableRow && localData.value.length > 0) {
// 第一列是否传了默认slot
const defaultSlot = localCols.value[0].defaultSlot
// 在第一列数据插槽中设置数据前的拖拽图标
localCols.value[0].defaultSlot = (_prop) => {
const row = _prop.row
return (
<DragIcon>
{defaultSlot ? (
defaultSlot(_prop)
) : (
<span>{localCols.value[0]?.prop ? row[localCols.value[0].prop] : ''}</span>
)}
</DragIcon>
)
}
}
})
onMounted(() => {
if (props.adaptive) {
setAdaptHeight()
}
if (props.draggableCol) {
columnDrop()
}
if (props.draggableRow) {
rowDrop()
}
})
// 默认值的属性添加到table-column上
function setColumnDefault(column: TableColumnType) {
return { ...columnDefaults, ...column }
}
// 设置table自适应高度,这样无论屏幕怎么变化,分页器始终在底部
async function setAdaptHeight() {
await nextTick()
if (props.adaptive) {
let offset = 58
// 如果分页高度是自己定义的
if (typeof props.adaptive === 'number') {
offset = props.adaptive
}
// table高度 = 屏幕高度 - table距离顶部的高度 - 分页高度58
const height = window.innerHeight - tableRef.value.$el.getBoundingClientRect().top - offset
tableRef.value.style.height = `${height}px`
}
}
// 设置防抖动
const fn = useDebounceFn(setAdaptHeight, 200)
// 监听table的resize事件,设置table自适应高度
useResizeObserver(tableRef, fn)
// 列拖拽实现
function columnDrop() {
nextTick(() => {
// 获取所有表格头中的列
const el = tableRef.value.$el.querySelector('.el-table__header-wrapper tr')
// 使用Sortable库实现列拖拽
Sortable.create(el, {
delay: 0,
animation: 300,
onEnd: ({ newIndex, oldIndex }) => {
// oldIndex为起始位置,newIndex为结束位置
// 在这里调整列的位置
// 先删除起始位置的列
const draggedItem = localCols.value.splice(oldIndex, 1)[0]
// 在结束位置插入列
localCols.value.splice(newIndex, 0, draggedItem)
// 传出拖拽后的所有列数据
emits('drag-col-change', localCols.value)
}
})
})
}
// 行拖拽实现
function rowDrop() {
nextTick(() => {
// 获取所有表格头中的列
const el = tableRef.value.$el.querySelector('.el-table__body-wrapper tbody')
// 使用Sortable库实现列拖拽
Sortable.create(el, {
delay: 0,
animation: 300,
// 拖拽图标的类名
handle: '.drag-btn',
onEnd: ({ newIndex, oldIndex }) => {
// oldIndex为起始位置,newIndex为结束位置
// 在这里调整列的位置
// 先删除起始位置的列
const draggedItem = localData.value.splice(oldIndex, 1)[0]
// 在结束位置插入列
localData.value.splice(newIndex, 0, draggedItem)
// 传出拖拽后的所有列数据
emits('drag-row-change', localData.value)
}
})
})
}
// 如果列数组中没有id,手动添加id
function addId(flag: boolean, arry: any[]) {
const ids = Math.random().toString(36).slice(2)
// flag:是否设置了列拖拽
if (flag && arry.length > 0 && !arry[0].id) {
arry.forEach((item, index) => {
item.id = ids+ '-'+index
})
rowKey.value = 'id'
}
return arry
}
</script>
列组件VTableColumn.vue
<template>
<el-table-column v-bind="props">
<!-- 设置el-table-column插槽,可以在scheme中配置对应的插槽内容 -->
<template #default="scope" v-if="defaultSlot">
<component v-bind="scope" :is="defaultSlot"></component>
</template>
<template #header="scope" v-if="headerSlot">
<component v-bind="scope" :is="headerSlot"></component>
</template>
<!-- 处理嵌套的多级表头 -->
<template v-if="children && children.length">
<v-table-column v-bind="item" v-for="(item, index) in children" :key="index"></v-table-column>
</template>
</el-table-column>
</template>
<script setup lang="ts">
import type { TableColumnType } from './types'
const props = defineProps<TableColumnType>()
</script>
<style scoped></style>
行拖拽图标组件DragIcon.vue
<template>
<div class="flex items-center drag-btn">
<i :class="['cursor-grab mr-2', icon]"></i>
<slot></slot>
</div>
</template>
<script setup lang="ts">
withDefaults(
defineProps<{
icon?: string
}>(),
{ icon: 'i-icon-park-outline:drag' }
)
</script>
<style scoped></style>
类型文件types.d.ts
import type { PaginationProps, TableColumnCtx, TableProps } from 'element-plus'
import { Component } from 'vue'
export interface TableColumnType extends TableColumnCtx<any> {
id?: string | number
defaultSlot?: typeof Component
headerSlot?: typeof Component
children?: TableColumnType[]
}
export interface PaginationType extends Partial<PaginationProps> {
align?: 'left' | 'center' | 'right'
total: number
defaultSlot?: typeof Component
}
export interface VTableProps extends TableProps<any> {
columns: TableColumnType[]
data: any[]
pagination?: PaginationType
// 是否自适应高度
adaptive?: boolean | number
loading?: boolean
// 是否可以拖拽列
draggableCol?: boolean
// 是否可以拖拽行
draggableRow?: boolean
}
export type TableEventsType = {
select: [selection: any, row: any]
'select-all': [selection: any]
'selection-change': [selection: any]
'cell-mouse-enter': [row: any, column: any, cell: any, event: Event]
'cell-mouse-leave': [row: any, column: any, cell: any, event: Event]
'cell-click': [row: any, column: any, cell: any, event: Event]
'cell-dblclick': [row: any, column: any, cell: any, event: Event]
'cell-contextmenu': [row: any, column: any, cell: any, event: Event]
'row-click': [row: any, column: any, event: Event]
'row-contextmenu': [row: any, column: any, event: Event]
'row-dblclick': [row: any, column: any, event: Event]
'header-click': [column: any, event: Event]
'header-contextmenu': [column: any, event: Event]
'sort-change': [{ column: any; prop: string; order: string }]
'filter-change': [{ key: string }, filters: any[]]
'current-change': [currentRow: any, oldCurrentRow: any]
'header-draged': [newWidth: number, oldWidth: number, column: any, event: Event]
'expand-change': [row: any, expandedRows: any[] | boolean]
}
// 分页中事件的回调函数
type PaginationCallFunc = (value: number) => void
// 分页组件emit出事件,因pagination中事件与table中事件重名,故重命名page-xx
export type PaginationEventsType = {
'page-size-change': [PaginationCallFunc]
'page-current-change': [PaginationCallFunc]
'page-prev-click': [PaginationCallFunc]
'page-next-click': [PaginationCallFunc]
}
// table拖拽事件
export type TableExtendEvents = {
'drag-row-change': [row: any],
'drag-col-change': [cols: any]
}
// table+pagination emit出事件
export type VTableEmitsType = TableEventsType & PaginationEventsType & TableExtendEvents
3.实现demo文件advanced-table.vue
<template>
<el-tabs v-model="activeName" class="demo-tabs">
<el-tab-pane label="自适应高度" name="first">
<VTable
:columns="columns"
:data="tableData"
:pagination="pagination"
adaptive
:loading="loading"
>
</VTable>
</el-tab-pane>
<el-tab-pane label="列拖拽" name="second">
<VTable
:columns="columns"
:data="tableData"
:pagination="pagination"
draggableCol
@drag-col-change="handleDragColChange"
>
</VTable>
</el-tab-pane>
<el-tab-pane label="行拖拽" name="third">
<VTable
:columns="columns1"
:data="tableData"
:pagination="pagination"
draggableRow
>
</VTable>
</el-tab-pane>
</el-tabs>
</template>
<script setup lang="ts">
definePage({
meta: {
title: 'pages.components.advanced-table',
icon: 'meteor-icons:table-layout'
}
})
const tableData = [
{
date: '2016-05-04',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles'
},
{
date: '2016-05-01',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles'
},
{
date: '2016-04-01',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles'
}
]
const activeName = ref('first')
const columns = [
{
label: '日期',
prop: 'date'
},
{
label: '姓名',
prop: 'name'
},
{
label: '地址',
prop: 'address'
}
]
const columns1 = [
{
label: '日期',
prop: 'date'
},
{
label: '姓名',
prop: 'name'
},
{
label: '地址',
prop: 'address'
}
]
const pagination = ref({
align: 'right',
size: 'default',
small: false,
background: false,
layout: 'total, sizes, prev, pager, next, jumper',
pageSizes: [10, 20, 30, 40, 50, 100],
total: 100
})
const loading = ref(false)
const handleDragColChange = (val: any) => {
console.log('🚀 ~ handleDragColChange ~ val:', val)
}
</script>
<style scoped></style>