基于 vxe-table 的高性能表格组件封装

1. 组件简介

在企业级中后台系统开发中,表格组件是最常用的数据展示形式之一。本文将介绍一个基于 vxe-table 封装的高性能表格组件 xTable,该组件集成了大数据渲染、行内编辑、列筛选等常用功能。

版本

vxe-pc-ui:4.5.14
vxe-table:4.12.5

2. 核心功能特性

  • 虚拟滚动,支持大数据渲染
  • 行内编辑功能
  • 列筛选功能
  • 数据导入导出
  • 自动列宽
  • 多选功能
  • 分页组件
  • 国际化支持

3. 组件封装代码

<template>
  <div class="vxe-table-container relative">
    <div class="border-gray-150 border">
      <div class="table-control bg-gray-120 flex items-center justify-between px-2.5 py-1">
        <el-checkbox v-model="autoColumnWidth">自动列宽</el-checkbox>
        <div class="flex">
          <div class="text-gray-560 ml-5 flex cursor-pointer items-center" @click="handleImport" v-if="showImport">
            <i class="iconfont icon-daoru mr-1 text-base"></i>
            <span>导入</span>
          </div>
          <div class="text-green-550 ml-5 flex cursor-pointer items-center" @click="handleExport" v-if="showExport">
            <i class="iconfont icon-daochu mr-1 text-base"></i>
            <span>导出</span>
          </div>
        </div>
      </div>
      <vxe-table
        ref="vxeTable"
        :height="tableHeight"
        :data="tableData"
        :show-overflow="!autoColumnWidth"
        :auto-resize="true"
        border
        :row-config="{ isCurrent: true, isHover: true }"
        :checkbox-config="{ range: true }"
        :column-config="{ resizable: true }"
        :edit-config="{ trigger: 'dblclick', mode: 'row' }"
        :virtual-y-config="{ enabled: true, gt: 2000 }"
        @edit-closed="handleEditClosed"
        @cell-click="handleCellClick"
        @checkbox-change="handleCheckboxChange"
      >
        <vxe-column type="seq" width="70" fixed="left">
          <template #header>
            <el-popover class="filterIcon" placement="bottom-start" trigger="click">
              <el-checkbox-group class="max-h-80 overflow-y-auto overflow-x-hidden" v-model="columnVal">
                <el-checkbox v-for="(v, i) in columns" :key="i" :value="v.field" :label="v.title" :disabled="v.disabled" class="dark:hover:bg-gray-560 w-full rounded pl-1"></el-checkbox>
              </el-checkbox-group>
              <template #reference>
                <el-button link><i class="iconfont icon-a-shaixuan1 text-xl"></i></el-button>
              </template>
            </el-popover>
          </template>
        </vxe-column>
        <vxe-column type="checkbox" v-if="showCheckBox" width="60" fixed="left"></vxe-column>
        <vxe-column
          v-for="(v, k) in columnData"
          :key="k"
          sortable
          :width="v.width ? v.width : autoColumnWidth ? 'auto' : ''"
          :field="v.field"
          :title="v.title"
          :edit-render="v.editRender"
          :visible="columnVal.includes(v.field)"
          :fixed="v.fixed"
        >
          <template v-if="v.type == 'custom'" #default="{ row }">
            <slot :name="v.slotName" :row="row">
              <!-- 默认内容(可选) -->
              {{ row[v.field] }}
            </slot>
          </template>
        </vxe-column>
        <template #empty>
          <div class="empty-data">
            <el-empty description="暂无数据" />
          </div>
        </template>
      </vxe-table>
    </div>
    <el-pagination
      class="mt-2"
      v-if="isPage"
      v-model:current-page="currentPage"
      v-model:page-size="pageSize"
      :total="total"
      background
      :page-sizes="[10, 20, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 50000]"
      layout="slot,->, sizes, prev, pager, next, jumper"
    >
      <div class="flex items-center">
        共 <span class="text-green-550 mx-1 text-base">{{ total }}</span> 条数据
      </div>
    </el-pagination>
  </div>
</template>

<script setup lang="ts">
import { ref, watch } from 'vue';
import type { PropType } from 'vue';
import type { Column } from './../types/common';
// 定义 props
const props = defineProps({
  data: {
    type: Array as PropType<any[]>,
    required: true,
  },
  columns: {
    type: Array as PropType<Column[]>,
    required: true,
  },
  showCheckBox: {
    type: Boolean,
    default: false,
  },
  showImport: {
    type: Boolean,
    default: false,
  },
  showExport: {
    type: Boolean,
    default: true,
  },
  isPage: {
    type: Boolean,
    default: true,
  },
  total: {
    type: Number,
    default: 0,
  },
});
let autoColumnWidth = ref(true); // 是否自动列宽
let columnVal = ref<string[]>([]); // 选中的列值
const tableData = ref<any[]>(props.data); // 表格数据响应式绑定
const columnData = ref<Column[]>(props.columns); // 表格列数据响应式绑定
const currentPage = ref(1); // 当前页码
const pageSize = ref(20); // 每页条数
const tableHeight = ref<number>(0); //  表格高度响应式绑定

// 定义 emits
const emit = defineEmits<{
  (e: 'import'): void;
  (e: 'export'): void;
  (e: 'cell-click', row: any): void;
  (e: 'checkbox-change', records: any[]): void;
  (e: 'page-change', currentPage: number): void;
  (e: 'size-change', currentPage: number, pageSize: number): void;
  (e: 'lineEdit', row: any): void;
}>();

// 监听 props.data 变化更新本地 tableData
watch(
  () => [props.data, props.columns],
  ([newData, newColumns]) => {
    tableData.value = newData;
    columnData.value = newColumns;
    columnVal.value = props.columns.filter((item) => item.visible == null || item.visible === true).map((item) => item.field);
  },
  { immediate: true, deep: true },
);

watch(
  () => currentPage.value,
  (newCurrentPage) => {
    emit('page-change', newCurrentPage);
  },
);
watch(
  () => pageSize.value,
  (newPageSize) => {
    currentPage.value = 1; // 重置当前页码为1
    emit('size-change', currentPage.value, newPageSize);
  },
);

// 方法定义
const handleImport = () => emit('import');
const handleExport = () => {
  // 触发事件通知外部已导出
  emit('export');
};
const handleEditClosed = ({ row }: { row: any }) => emit('lineEdit', row);
const handleCellClick = ({ row }: { row: any }) => emit('cell-click', row);
const handleCheckboxChange = ({ records }: { records: any[] }) => emit('checkbox-change', records);

onMounted(() => {
  // 获取表格高度
  const container = document.querySelector<HTMLElement>('.vxe-table-container');
  if (container == null) return;
  const height = props.isPage ? 40 + 40 : 40;
  tableHeight.value = container.offsetHeight - height;
});
</script>

<style scoped>
.vxe-table-container {
  margin-top: 16px;
}
</style>

4. 类型定义common.ts

import type { VxeTableSelectProps } from 'vxe-pc-ui';
export interface Column {
  field: string; // 列字段名
  title: string; // 列标题
  visible?: boolean; // 是否默认显示
  disabled?: boolean; // 是否禁用控制显示隐藏
  type?: string; // 列类型 custom:自定义列
  slotName?: string; // 列插槽
  width?: number; // 列宽度
  fixed?: 'left' | 'right'; // 是否固定列
  editRender?: {
    name?: string;
    props?: Partial<VxeTableSelectProps>;
    defaultValue?: any;
    immediate?: boolean;
    events?: {
      [key: string]: (...args: any[]) => void;
    };
  };
}

5. 使用示例

<template>
  <xTable
    :data="tableData"
    :showCheckBox="true"
    :showImport="true"
    :total="5002"
    :columns="columns"
    style="height: calc(100% - 20px)"
    @page-change="changePage"
    @size-change="changeSize"
    @checkbox-change="changeSelected"
    @export="handleExport"
  >
  </xTable>
</template>
<script lang="ts" setup>
import { exportExcel } from './../../utils/table'; // 导入导出Excel的工具函数
// 定义表格数据的类型
interface User {
  name: string;
  role: string;
  age: number;
  address: string;
}
const tableData = ref<User[]>([]);
const columns = ref([
  { field: 'name', title: 'Name', type: 'custom', slotName: 'name' },
  {
    field: 'role',
    title: 'Role',
    editRender: {
      name: 'VxeSelect',
      options: [
        { value: 'Admin', label: 'Admin' },
        { value: 'User', label: 'User' },
        { value: 'Manager', label: 'Manager' },
        { value: 'Designer', label: 'Designer' },
        { value: 'Developer', label: 'Developer' },
      ],
    },
  },
  {
    field: 'age',
    title: 'Age',
  },
  { field: 'address', title: 'Address' },
]);
// 随机生成用户数据的函数
const generateRandomUser = (index: number): User => {
  const names = ['John Doe', 'Jane Smith', 'Alice Johnson', 'Bob Brown'];
  const roles = ['Admin', 'User', 'Manager', 'Designer', 'Developer'];
  const addresses = ['Main St', 'Elm St', 'Oak St', 'Pine St', 'Maple St'];

  // 每隔 500 行生成一个特别长的 address
  let address = `${Math.floor(Math.random() * 1000)} ${addresses[Math.floor(Math.random() * addresses.length)]}`;
  if (index % 500 === 0) {
    address = '这是一个特别长的地址信息,用于测试性能和显示效果。'.repeat(20);
  }

  return {
    name: names[index % names.length],
    role: roles[Math.floor(Math.random() * roles.length)],
    age: Math.floor(Math.random() * 10 + 18), // 年龄在18-28之间
    address,
  };
};
const changeSelected = (selected: User[]) => {
  console.log('Selected users:', selected);
};
const handleExport = () => {
  exportExcel(tableData, columns, 'users.xlsx');
};
const changePage = (currentPage: number) => {
  console.log('Current page:', currentPage);
};
const changeSize = (currentPage: number, pageSize: number) => {
  console.log('Current page:', currentPage, 'Page size:', pageSize);
};
onMounted(() => {
  const users: User[] = [];
  for (let i = 0; i < 3000; i++) {
    users.push(generateRandomUser(i));
  }
  tableData.value = users;
});
</script>
<style lang="less" scoped></style>

6. Props 说明

属性名 类型 默认值 说明
data Array [] 表格数据
columns Array [] 列配置
showCheckBox Boolean false 显示多选框
showImport Boolean false 显示导入按钮
showExport Boolean true 显示导出按钮
isPage Boolean true 显示分页
total Number 0 数据总数

7. Events 说明

事件名 说明 参数
page-change 页码改变 currentPage
size-change 页大小改变 (currentPage, pageSize)
checkbox-change 选择改变 selectedRecords
export 导出数据 -
lineEdit 行编辑完成 editedRow

8. 特色功能实现

8.1 虚拟滚动

:virtual-y-config="{ enabled: true, gt: 2000 }"

当数据量超过2000条时自动启用虚拟滚动。

8.2 自动列宽

:width="v.width ? v.width : autoColumnWidth ? 'auto' : ''"

通过 autoColumnWidth 控制列宽自动适配。

8.3 列筛选

const columnVal = ref<string[]>([]);
watch(() => props.columns, (newColumns) => {
  columnVal.value = newColumns
    .filter(item => item.visible == null || item.visible === true)
    .map(item => item.field);
}, { immediate: true });

9. 性能优化

  • 虚拟滚动处理大数据
  • 按需加载列
  • 自动列宽优化
  • 数据响应式处理优化

10. 注意事项

  • 大数据量时建议开启虚拟滚动
  • 自定义列需提供对应插槽
  • 编辑功能需配置正确的 editRender
  • 列宽自动适配可能影响性能
posted @ 2025-05-16 15:24  火炬冬天  阅读(267)  评论(0)    收藏  举报