通用管理后台组件库-9-高级表格组件

高级表格组件

说明:el-table的二次封装,实现自适应高度、列和行拖拽。

1.实现效果

2.封装的表格VTable.vue

xml 复制代码
<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

xml 复制代码
<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

xml 复制代码
<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

typescript 复制代码
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

ini 复制代码
<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>
相关推荐
阿虎儿2 小时前
React Hook 入门指南
前端·react.js
核以解忧2 小时前
借助VTable Skill实现10W+数据渲染
前端
WangHappy2 小时前
不写 Canvas 也能搞定!小程序图片导出的 WebView 通信方案
前端·微信小程序
李剑一2 小时前
要闹哪样?又出现了一款新的格式化插件,尤雨溪力荐,速度提升了惊人的45倍!
前端·vue.js
闲云一鹤2 小时前
Git LFS 扫盲教程 - 你不会还在用 Git 管理大文件吧?
前端·git·前端工程化
阿虎儿3 小时前
React Context 详解:从入门到性能优化
前端·vue.js·react.js
Sailing3 小时前
🚀 别再乱写 16px 了!CSS 单位体系已经进入“计算时代”,真正的响应式布局
前端·css·面试
喝水的长颈鹿3 小时前
【大白话前端 03】Web 标准与最佳实践
前端
爱泡脚的鸡腿3 小时前
Node.js 拓展
前端·后端