引言
在 Vue 3 + Element Plus 的项目开发中,表格(Table)是最常用的组件之一。为了提升开发效率,减少重复代码,我们常常需要对 Element Plus 的 Table 组件进行二次封装。本文将介绍一个实用的 Table 组件封装方案,该方案集成了分页、选择、自定义列等功能,可以显著提升开发效率。
组件特点
- 灵活的自适应高度
- 自动计算表格高度,适应不同页面布局
- 支持自定义高度设置
- 响应式调整,适应窗口大小变化
- 丰富的列类型支持
- 支持图片列(可预览)
- 支持自定义 HTML 渲染
- 支持自定义插槽
- 支持可点击的按钮列
- 支持自定义样式
- 完善的选择功能
- 支持多选和单选模式
- 支持自定义选择条件
- 支持点击行选择
- 支持保留选择状态
- 内置分页功能
- 支持页码切换
- 支持每页条数调整
- 支持总数显示
- 支持快速跳转
js
<template>
<div class="table-header">
<div>
<slot name="left" />
</div>
<div>
<slot name="right" />
</div>
</div>
<el-table
:data="data"
stripe
@selection-change="handleSelectionChange"
@row-click="handleCurrentChange"
:height="height || `calc(100vh - ${subtractHeight}px`"
v-loading="loading"
ref="tableRef"
class="yh-table"
:class="single ? 'isSingle' : ''"
:row-key="rowKey"
>
<el-table-column
type="selection"
width="55"
v-if="selection"
:selectable="selectable"
:reserve-selection="reserveSelection"
fixed="left"
align="center"
/>
<el-table-column
type="index"
width="80"
align="center"
label="序号"
:index="indexMethod"
v-if="index"
/>
<el-table-column
v-for="col in columns"
:prop="col.prop"
:key="col.prop"
:label="col.label"
:min-width="col.minWidth"
:width="col.width"
show-overflow-tooltip
:formatter="col.formatter"
align="center"
:fixed="col.fixed"
>
<template #default="{ row }">
<div v-if="col.type === 'img'">
<el-image
v-for="img in row[col.prop]?.split(',')"
:src="baseUrl + '/common/previewImage?avatar=' + img"
style="width: 50px"
preview-teleported
:preview-src-list="[baseUrl + '/common/previewImage?avatar=' + img]"
>
<template #error>
<span></span>
</template>
</el-image>
</div>
<div v-else-if="col.html" v-html="col.html(row)"></div>
<slot v-else-if="col.slot" :name="col.slot" :row="row" />
<div v-else-if="col.click">
<el-button link type="primary" size="small" @click="col.click(row)">
{{ row[col.prop] }}
</el-button>
</div>
<span v-else :style="col.style">
{{ col.formatter ? col.formatter(row) : row[col.prop] }}
</span>
</template>
</el-table-column>
<slot></slot>
</el-table>
<div class="yh-pagination" v-if="showPagination">
<el-pagination
@size-change="sizeChange"
@current-change="currentChange"
:currentPage="modelValue.pageNum"
:page-size="modelValue.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
size="small"
/>
</div>
</template>
<script setup>
import { reactive } from 'vue';
const baseUrl = import.meta.env.VITE_PROXY;
const props = defineProps({
data: {
type: Array,
default: () => []
},
columns: {
type: Array,
default: () => []
},
//是否多选
selection: {
type: Boolean,
default: false
},
total: {
type: Number,
default: 0
},
loading: {
type: Boolean,
default: false
},
//自定义表格高度
height: {
type: String,
default: undefined
},
//额外需要减去的高度
offsetHeight: {
type: Number,
default: 0
},
//分页
modelValue: {
type: Object,
default: () => {
return {
pageNum: 1,
pageSize: 20
};
}
},
//是否可选判断条件
selectable: {
type: Function,
default: () => true
},
//是否展开搜索栏
showSearch: {
type: Boolean,
default: false
},
//单选
single: {
type: Boolean,
default: false
},
reserveSelection: {
type: Boolean,
default: false
},
rowKey: {
type: String,
default: ''
},
//是否展示序号
index: {
type: Boolean,
default: true
},
//是否显示分页
showPagination: {
type: Boolean,
default: true
},
//是否需要点击单元格选中数据
needClickCell: {
type: Boolean,
default: true
}
});
const selectedList = ref([]);
const handleCurrentChange = (row) => {
if (!props.selection || !props.needClickCell || !props.rowKey) return;
const index = selectedList.value.findIndex(
(item) => item[props.rowKey] === row[props.rowKey]
);
if (index === -1) {
selectedList.value.push(row);
tableRef.value.toggleRowSelection(row, true);
} else {
selectedList.value.splice(index, 1);
tableRef.value.toggleRowSelection(row, false);
}
};
const emits = defineEmits([
'update:modelValue',
'pageChange',
'selection-change'
]);
const tableRef = ref(null);
//序号
const indexMethod = (index) => {
return props.modelValue.pageSize * (props.modelValue.pageNum - 1) + index + 1;
};
//多选
const handleSelectionChange = (val) => {
if (props.single && val.length > 1) {
tableRef.value.clearSelection();
tableRef.value.toggleRowSelection(val.pop(), true);
return;
}
selectedList.value = val;
emits('selection-change', val);
};
//分页
const sizeChange = (val) => {
const page = { ...props.modelValue };
page.pageSize = val;
emits('update:modelValue', page);
emits('pageChange');
};
const currentChange = (val) => {
const page = { ...props.modelValue };
page.pageNum = val;
emits('update:modelValue', page);
emits('pageChange');
};
onMounted(() => {
getSearchFormHeight();
window.addEventListener('resize', getSearchFormHeight);
});
//table需要减去的高度
const subtractHeight = ref(50);
const getSearchFormHeight = () => {
nextTick(() => {
// 页面头部高度
const pageHeaderHeight =
document.querySelector('.main-hearder-top')?.offsetHeight || 0;
// 分页高度
const pageHeight =
document.querySelector('.yh-pagination')?.offsetHeight || 0;
// 搜索表单高度
const searchformHeight =
document.querySelector('.search-form')?.offsetHeight || 0;
// 工具栏高度
const toolsHeight =
document.querySelector('.table-header')?.offsetHeight || 0;
// 减去的高度 35=>页面内边距padding值
subtractHeight.value =
searchformHeight + pageHeight + pageHeaderHeight + toolsHeight + 43;
});
};
watch(
() => props.showSearch,
() => {
getSearchFormHeight();
}
);
onUnmounted(() => {
window.removeEventListener('resize', getSearchFormHeight);
});
</script>
<style lang="scss" scoped>
.table-header {
display: flex;
justify-content: space-between;
padding: 5px 0;
}
.yh-pagination {
display: flex;
justify-content: flex-end;
margin-top: 10px;
}
.isSingle :deep(.el-table__header-wrapper .el-checkbox) {
display: none;
}
</style>
示例
js
<template>
<div class="table-container">
<!-- 搜索表单 -->
<div class="search-form">
<el-form :inline="true">
<el-form-item label="姓名">
<el-input v-model="searchForm.name" placeholder="请输入姓名" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">搜索</el-button>
<el-button @click="resetSearch">重置</el-button>
</el-form-item>
</el-form>
</div>
<!-- 表格工具栏 -->
<div class="table-header">
<div>
<el-button type="primary" @click="handleAdd">新增</el-button>
<el-button type="danger" @click="handleBatchDelete">批量删除</el-button>
</div>
<div>
<el-button @click="handleExport">导出</el-button>
</div>
</div>
<!-- 表格 -->
<yh-table
:data="tableData"
:columns="columns"
:total="total"
v-model:modelValue="page"
selection
:row-key="'id'"
:loading="loading"
@selection-change="handleSelectionChange"
@pageChange="handlePageChange"
>
<template #operation="{ row }">
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
</template>
</yh-table>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
// 搜索表单
const searchForm = ref({
name: ''
});
// 表格数据
const tableData = ref([]);
const loading = ref(false);
const total = ref(0);
// 分页数据
const page = ref({
pageNum: 1,
pageSize: 20
});
// 选中的数据
const selectedRows = ref([]);
// 列配置
const columns = [
{
prop: 'name',
label: '姓名',
width: '120'
},
{
prop: 'avatar',
label: '头像',
type: 'img'
},
{
prop: 'age',
label: '年龄',
width: '100'
},
{
prop: 'status',
label: '状态',
formatter: (row) => row.status === 1 ? '正常' : '禁用'
},
{
prop: 'score',
label: '分数',
style: (row) => ({
color: row.score >= 60 ? 'green' : 'red',
fontWeight: 'bold'
})
},
{
prop: 'detail',
label: '详情',
click: (row) => {
console.log('查看详情:', row);
}
},
{
prop: 'operation',
label: '操作',
slot: 'operation'
}
];
// 方法
const fetchData = async () => {
loading.value = true;
try {
// 模拟API调用
const response = await fetchDataFromApi({
...searchForm.value,
...page.value
});
tableData.value = response.data;
total.value = response.total;
} catch (error) {
console.error('获取数据失败:', error);
} finally {
loading.value = false;
}
};
const handleSearch = () => {
page.value.pageNum = 1;
fetchData();
};
const resetSearch = () => {
searchForm.value = {
name: ''
};
handleSearch();
};
const handleSelectionChange = (selection) => {
selectedRows.value = selection;
};
const handleAdd = () => {
console.log('新增');
};
const handleEdit = (row) => {
console.log('编辑:', row);
};
const handleDelete = (row) => {
console.log('删除:', row);
};
const handleBatchDelete = () => {
if (selectedRows.value.length === 0) {
ElMessage.warning('请选择要删除的数据');
return;
}
console.log('批量删除:', selectedRows.value);
};
const handleExport = () => {
console.log('导出');
};
// 生命周期
onMounted(() => {
fetchData();
});
</script>
<style lang="scss" scoped>
.table-container {
padding: 20px;
.search-form {
margin-bottom: 20px;
}
.table-header {
margin-bottom: 10px;
}
}
</style>