基于 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
- 列宽自动适配可能影响性能
浙公网安备 33010602011771号