Element-Plus 虚拟表格ElTableV2组件使用

1,489 阅读3分钟

1、版本

虚拟表格组件要求组件库最低版本为2.2.0 虚拟表格

2、了解主要属性、API

Table属性说明默认值
header-heightHeader 的高度由height设置。 如果传入数组,它会使 header row 等于数组长度50
row-height每行的高度, 用于计算表的总高度50
columns列 column 的配置数组-
data要在表中渲染的数据数组-
fixed单元格宽度是自适应还是固定false
width表格宽度-
height表格高度-
column属性说明默认值
align表格单元格内容对齐方式left
hidden此列是否不可见-
style自定义列单元格的类名,将会与 gird 单元格合并-
titleHeader 头部单元格中的默认文本-
width列宽度-
cellRenderer自定义单元格渲染器-
headerCellRenderer自定义头部渲染器-
插槽参数
rowRowSlotProps
cell CellSlotProps

3、使用

      <te-table-v2
        v-if="!loading"
        fixed
        stripe
        :row-height="40"
        :header-height="40"
        :columns="columns"
        :data="dataSource"
        :width="width"
        :height="height"
      >
        <template #row="props">
          <Row v-bind="props" />
        </template>
      </te-table-v2>
列渲染
/**
 * 列
 */
const columns = ref<Column<any>[]>([
  {
    key: 'text',
    dataKey: 'text',
    title: '测试文本',
    width: 90,
    cellRenderer: ({ cellData: text }: any) => `测试-${text}`,
    rowSpan: ({ rowIndex }: any) => {
      const row = dataSource.value[rowIndex];
      const firstIndex = dataSource.value?.findIndex((item) => item.month === row.month);
      return firstIndex === rowIndex ? monthCountMap.value.get(row.month + '') ?? 1 : 0;
    },
  },
  {
    key: 'name',
    dataKey: 'name',
    title: '测试',
    hidden: currentType.value,
    width: 400,
    cellRenderer: ({ cellData: name, rowData: row }: any) =>
      editing.value
        ? h(
            ElSelect,
            {
              modelValue: row.id,
              clearable: true,
              placeholder: '请选择',
              ['onUpdate:modelValue']: (value: number) => {
                row.id = value;
              },
              onChange: () => handleInputChange(),
            },
            () =>
              list.value?.map((item) =>
                h(ElOption, {
                  value: item.id,
                  label: item.name,
                }),
              ) ?? [],
          )
        : h(
            'span',
            {},
            {
              default: () => (row?.name ? row?.name : '-'),
            },
          ),
  },
  {
    key: 'count',
    dataKey: 'count',
    title: '数值',
    align: 'right',
    width: 160,
    cellRenderer: ({ cellData: count, rowData: row }: any) =>
      editing.value
        ? withDirectives(
            h(TeInput, {
              modelValue: count,
              clearable: true,
              placeholder: '请输入',
              ['onUpdate:modelValue']: (value: string) => {
                row.count = value;
              },
              onInput: () => handleInputChange(),
            }),
            [[inputFilterDirective, { decimal: 2 }, 'number']],
          )
        : h(
            'span',
            {},
            {
              default: () => (row?.count ? row?.count : '-'),
            },
          ),
  },
  {
    key: 'operate',
    dataKey: 'operate',
    title: '操作',
    width: 64,
    cellRenderer: ({ rowData: row, rowIndex: rowIndex }: any) =>
      h(
        ElButton,
        {
          type: 'primary',
          link: true,
          disabled: mapDeleteDisabled(row),
          onClick: () => handleSingleDelete(row, rowIndex),
        },
        {
          default: () => '删除',
        },
      ),
  },
  {
    key: 'operate-2',
    dataKey: 'operate-2',
    title: '操作',
    width: 64,
    cellRenderer: ({ rowData: row }: any) =>
      h(
        ElButton,
        {
          type: 'primary',
          link: true,
          onClick: () => handleSingleAdd(row),
        },
        {
          default: () => '新增',
        },
      ),
    rowSpan: ({ rowIndex }: any) => {
      const row = dataSource.value[rowIndex];
      const firstIndex = dataSource.value?.findIndex((item) => item.month === row.month);
      return firstIndex === rowIndex ? monthCountMap.value.get(row.month + '') ?? 1 : 0;
    },
    headerCellRenderer: () => h('span', { default: () => '' }),
  },
]);
合并单元格
const Row = ({ rowData, rowIndex, cells, columns }: any) => {
  const rowSpan = columns[0].rowSpan({ rowData, rowIndex });
  if (rowSpan > 1) {
    const cell = cells[0];
    const style = {
      ...cell.props.style,
      height: `${rowSpan * 40 - 1}px`,
      alignSelf: 'flex-start',
      zIndex: 1,
      backgroundColor: '#ffffff',
    };
    cells[0] = cloneVNode(cell, { style });
  }
  return cells;
};

4、难点

1、hidden控制列隐藏 如果在初始化赋值columns设置了hidden等于一个表达式,重新渲染时是不会生效的,我是在重新渲染时给hidden重新赋值了,可以试试hidden赋值一个箭头函数

2、rowSpan字段是控制表格单元格合并的,支持传入一个函数返回rowSpan

3、单元格动态渲染cellRenderer需要返回VNode 这里可以去学习下Vue中h函数的使用,我这里用到了普通文本、输入框、下拉框的渲染

// 完整参数签名 
  function h( 
    type: string | Component,
    props?: object | null, 
    children?: Children | Slot | Slots 
   ): VNode

第一个参数既可以是一个字符串 (用于原生元素) 也可以是一个 Vue 组件定义。第二个参数是要传递的 prop,第三个参数是子节点。 当创建一个组件的 vnode 时,子节点必须以插槽函数进行传递。如果组件只有默认槽,可以使用单个插槽函数进行传递。否则,必须以插槽函数的对象形式来传递。

其中比较难的是h函数中指令的使用,Vue提供了一个withDirectives的API

   function withDirectives( 
    vnode: VNode, 
    directives: DirectiveArguments 
    ): VNode 
    
 // [Directive, value, argument, modifiers] 
   type DirectiveArguments = Array< 
     | [Directive] 
     | [Directive, any] 
     | [Directive, any, string] 
     | [Directive, any, string, DirectiveModifiers] 
     >

用自定义指令包装一个现有的 vnode。第二个参数是自定义指令数组。每个自定义指令也可以表示为 [Directive, value, argument, modifiers] 形式的数组。如果不需要,可以省略数组的尾元素。 比如这里我用了我自定义的指令inputFilterDirective,后面两个参数是我给指令的传参

[[inputFilterDirective, { decimal: 2 }, 'number']],
转换到template模板语法就是v-inputFilterDirective:number="{ decimal: 2 }"

4、使用row插槽实现单元格合并以及自定义样式

const Row = ({ rowData, rowIndex, cells, columns }: any) => {
    return cells
}

搭配row插槽使用

        <template #row="props">
          <Row v-bind="props" />
        </template>

5、问题

使用的过程中也发现了几个问题 1、貌似不支持斑马纹,如果设置了stripe="true",合并的单元格依然是白色背景,而没有合并的单元全都是灰色

2、已合并行的单元格在滚动到顶部是只留下几行在可视区域,会出现不合并的情况。