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

2,426 阅读6分钟

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、使用

eg1:
      <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;
};


eg2:
// 虚拟表格列
const virtualColumnList = ref<Column<any>[]>([
  {
    key: 'sort',
    dataKey: 'sort',
    fixed: 'left' as any,
    title: '序号',
    width: 64,
    cellRenderer: ({ rowIndex }: any): any => `${rowIndex + 1}`,
  },
  {
    key: 'pointId',
    dataKey: 'pointId',
    fixed: 'left' as any,
    title: '点位名称',
    headerCellRenderer: () =>
      h(
        'div',
        {
          className: 'arcd-custom-header',
        },
        ['点位名称'],
      ),
    width: 180,
    cellRenderer: ({ rowData: row }: any): any =>
      h(
        TeSelect,
        {
          modelValue: row.pointId,
          clearable: false,
          placeholder: '请选择',
          ['onUpdate:modelValue']: (value: number) => {
            row.pointId = value;
          },
          onChange: (value: number) => handlePointChange(value, row),
        },
        () =>
          pointList.value?.map((item) =>
            h(TeOption, { value: item.pointId, label: item.name }),
          ) ?? [],
      ),
  },
  {
    key: 'code',
    dataKey: 'code',
    title: '编码',
    width: 180,
    cellRenderer: ({ rowData: row }: any) =>
      h(TeInput, {
        modelValue: row.code,
        disabled: true,
      }),
  },
  {
    key: 'triggerLogic',
    dataKey: 'triggerLogic',
    title: '触发逻辑',
    hidden: mapIsDataAlarmRule(currentSelectModule.value),
    headerCellRenderer: () =>
      h(
        'div',
        {
          className: 'arcd-custom-header',
        },
        ['触发逻辑'],
      ),
    width: 128,
    cellRenderer: ({ rowData: row }: any): any =>
      h(
        TeSelect,
        {
          modelValue: row.triggerLogic,
          clearable: false,
          placeholder: '请选择',
          ['onUpdate:modelValue']: (value: number) => {
            row.triggerLogic = value;
          },
          onChange: () => handleTriggerLogicChange(row),
        },
        () =>
          triggerLogicTypeOptions?.map((item) =>
            h(TeOption, { value: item.value, label: item.label }),
          ) ?? [],
      ),
  },
  {
    key: 'limitType',
    dataKey: 'limitType',
    title: '越限类别',
    hidden: mapIsStatusAlarmRule(currentSelectModule.value),
    headerCellRenderer: () =>
      h(
        'div',
        {
          className: 'arcd-custom-header',
        },
        ['越限类别'],
      ),
    width: 160,
    cellRenderer: ({ rowData: row }: any): any =>
      h(
        TeSelect,
        {
          modelValue: row.limitType,
          clearable: false,
          placeholder: '请选择',
          ['onUpdate:modelValue']: (value: number) => {
            row.limitType = value;
          },
          onChange: () => handleLimitTypeChange(row),
        },
        () =>
          overrunTypeOptions?.map((item) =>
            h(TeOption, { value: item.value, label: item.label }),
          ) ?? [],
      ),
  },
  {
    key: 'faultTypeName',
    dataKey: 'faultTypeName',
    title: '故障类别',
    width: 160,
    cellRenderer: ({ rowData: row }: any): any =>
      h(TeSelect, {
        modelValue: row.faultTypeName,
        clearable: false,
        placeholder: '请选择',
        disabled: true,
      }),
  },
  {
    key: 'overrunValue',
    dataKey: 'overrunValue',
    title: '越限告警值',
    hidden: mapIsStatusAlarmRule(currentSelectModule.value),
    width: 102,
    cellRenderer: ({ rowData: row }: any): any =>
      withDirectives(
        h(TeInput, {
          modelValue: row.overrunValue,
          clearable: false,
          placeholder: '请输入',
          ['onUpdate:modelValue']: (value: number) => {
            row.overrunValue = value;
          },
        }),
        [[inputFilterDirectiveInstance, { negative: true }, 'numberV2']],
      ),
  },
  {
    key: 'deadValue',
    dataKey: 'deadValue',
    title: '死区值',
    hidden: mapIsStatusAlarmRule(currentSelectModule.value),
    width: 102,
    cellRenderer: ({ rowData: row }: any): any =>
      withDirectives(
        h(TeInput, {
          modelValue: row.deadValue,
          clearable: false,
          placeholder: '请输入',
          ['onUpdate:modelValue']: (value: number) => {
            row.deadValue = value;
          },
        }),
        [[inputFilterDirectiveInstance, { negative: true }, 'numberV2']],
      ),
  },
  {
    key: 'duration',
    dataKey: 'duration',
    title: '触发延时',
    headerCellRenderer: () =>
      h(
        'div',
        {
          className: 'arcd-custom-header',
        },
        ['触发延时'],
      ),
    width: 112,
    cellRenderer: ({ rowData: row }: any): any =>
      withDirectives(
        h(
          TeInput,
          {
            modelValue: row.duration,
            clearable: false,
            placeholder: '请输入',
            ['onUpdate:modelValue']: (value: number) => {
              row.duration = value;
            },
          },
          {
            suffix: () => h('span', null, 's'),
          },
        ),
        [
          [
            inputFilterDirectiveInstance,
            { negative: false, integral: 4, decimal: 0, maxValue: 3600 },
            'numberV2',
          ],
        ],
      ),
  },
  {
    key: 'levelCode',
    dataKey: 'levelCode',
    title: '告警等级',
    headerCellRenderer: () =>
      h(
        'div',
        {
          className: 'arcd-custom-header',
        },
        ['告警等级'],
      ),
    width: 160,
    cellRenderer: ({ rowData: row }: any): any =>
      h(
        TeSelect,
        {
          modelValue: row.levelCode,
          clearable: false,
          placeholder: '请选择',
          ['onUpdate:modelValue']: (value: number) => {
            row.levelCode = value;
          },
        },
        () =>
          alarmLevelList.value?.map((item) =>
            h(TeOption, { value: item.code, label: item.name }),
          ) ?? [],
      ),
  },
  {
    key: 'content',
    dataKey: 'content',
    title: '告警文本',
    width: 228,
    showOverflowEllipsis: true,
    cellRenderer: ({ rowData: row, rowIndex }: any) =>
      h(
        'div',
        {
          className: 'arcd-edit-cell',
        },
        [
          h(
            'span',
            {
              title: row.content,
            },
            row.content,
          ),
          h(
            TeButton,
            {
              link: true,
              type: 'primary',
              onClick: () =>
                handleEdit(rowIndex, EEditLabelKey.告警文本, row.content),
            },
            () => '编辑',
          ),
        ],
      ),
  },
  {
    key: 'suggest',
    dataKey: 'suggest',
    title: '告警处理建议',
    width: 228,
    cellRenderer: ({ rowData: row, rowIndex }: any) =>
      h(
        'div',
        {
          className: 'arcd-edit-cell',
        },
        [
          h(
            'span',
            {
              title: row.suggest,
            },
            () => row.suggest,
          ),
          h(
            TeButton,
            {
              link: true,
              type: 'primary',
              onClick: () =>
                handleEdit(rowIndex, EEditLabelKey.告警建议, row.suggest),
            },
            () => '编辑',
          ),
        ],
      ),
  },
  {
    key: 'effectModeId',
    dataKey: 'effectModeId',
    title: '生效时间',
    width: 144,
    cellRenderer: ({ rowData: row }: any): any =>
      h(
        TeSelect,
        {
          disabled: true,
          modelValue: row.effectModeId,
          clearable: false,
          placeholder: '请选择',
          ['onUpdate:modelValue']: (value: number) => {
            row.effectModeId = value;
          },
          onChange: (value: number) => handlePointChange(value, row),
        },
        () =>
          workModeList.value?.map((item) =>
            h(TeOption, { value: item.id, label: item.name }),
          ) ?? [],
      ),
  },
  {
    key: 'enabledFlag',
    dataKey: 'enabledFlag',
    title: '启用状态',
    width: 88,
    cellRenderer: ({ rowData: row }: any) =>
      h(TeSwitch, {
        modelValue: row.enabledFlag,
        activeValue: ECommonStartStopStatusType.启用,
        inactiveValue: ECommonStartStopStatusType.停用,
        onChange: (value: any) => {
          row.enabledFlag = value;
        },
      }),
  },
  {
    key: 'publishStatus',
    dataKey: 'publishStatus',
    title: '发布状态',
    width: 88,
    cellRenderer: ({ rowData: row }: any) =>
      h(
        TeBadge,
        {
          isStatus: true,
          type: mapReleaseStatusType(row.publishStatus),
        },
        () => mapReleaseStatusLabel(row.publishStatus),
      ),
  },
  {
    key: 'operate',
    dataKey: 'operate',
    title: '操作',
    width: 136,
    cellRenderer: ({ rowIndex }: any) => {
      return h('div', { class: 'arcd-operation-buttons' }, [
        // 渲染 ElDropdown 下拉菜单
        h(
          TeDropdown,
          {
            onCommand: (value: string) => handleCommand(value, rowIndex),
          },
          {
            default: () =>
              h(
                TeButton,
                {
                  type: 'primary',
                  link: true,
                },
                {
                  default: () =>
                    h('div', null, [
                      h('span', null, '操作'),
                      h(IconDown, null),
                    ]),
                },
              ),
            dropdown: () =>
              h(TeDropdownMenu, null, {
                default: () => [
                  h(
                    TeDropdownItem,
                    {
                      command: 'up',
                      disabled: rowIndex === 0,
                    },
                    {
                      default: () => '上移',
                    },
                  ),
                  h(
                    TeDropdownItem,
                    {
                      command: 'down',
                      disabled: mapDownBtnDisabled(rowIndex),
                    },
                    {
                      default: () => '下移',
                    },
                  ),
                  h(
                    TeDropdownItem,
                    {
                      command: 'move',
                    },
                    {
                      default: () => '移动',
                    },
                  ),
                ],
              }),
          },
        ),
        // 渲染 TePopconfirm 确认框
        h(
          TePopconfirm,
          {
            title: '确认删除这条数据吗?',
            confirmButtonText: '确定',
            cancelButtonText: '取消',
            onConfirm: () => handleDeleteConfirm(rowIndex),
          },
          {
            reference: () =>
              h(
                TeButton,
                {
                  type: 'primary',
                  link: true,
                },
                {
                  default: () => '删除',
                },
              ),
          },
        ),
      ]);
    },
  },
]);

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、已合并行的单元格在滚动到顶部是只留下几行在可视区域,会出现不合并的情况。

3、控制台警告提示 Non-function value encountered for default slot 解决办法:h函数的第三个参数使用箭头函数

    h(
        TeBadge,
        {
          isStatus: true,
          type: mapReleaseStatusType(row.publishStatus),
        },
        () => mapReleaseStatusLabel(row.publishStatus),
      ),