本文最后更新于 7 天前,如有失效请评论区留言。
优雅的解决Vue+ElementUI项目中的分页勾选问题
小鹿最近的项目中涉及到很多表格的操作,我去看了一下el-table的文档解释,官方组件似乎并不支持分页勾选,但是项目又需要这个效果,所以小鹿就干脆自己写了一个公共组件,记录一下,方便后期使用。
业务场景
所谓分页勾选就是说,用户在表格中勾选了一条数据,当切换到第二页的时候,再勾选某些数据,这时候不能将第一页已经勾选的数据忘记,在用户翻回第一页的时候依旧需要是选中状态,具体效果如下所示:
另外,由于表格要求高度可自定义,因此,所有el-table的参数都应当支持参数传入,并且都需要一个默认值,所以我将其写成了一个公共组件。
代码实现
首先是表格组件代码,小鹿这里用的是vue3+TypeScript语法,当然从本质上来说TypeScript也要编译成JavaScript才能在NodeJs上面运行,所以如果你们愿意用Js也是没有问题的,网络上应该有很多转换工具,小鹿自己也打算写一个在线代码转换工具。
表格组件
<template>
<div class="custom-table-container">
<el-table
ref="tableRef"
:data="currentPageData"
:border="border"
:stripe="stripe"
:row-key="getRowKey"
style="width: 100%"
@select="handleSelectionChange"
@select-all="handleSelectionChange"
v-bind="$attrs"
>
<!-- 选择列 -->
<el-table-column
v-if="showSelection"
type="selection"
width="55"
:selectable="rowSelectable"
/>
<!-- 序号列 -->
<el-table-column v-if="showIndex" type="index" label="序号" width="60" />
<!-- 动态列 -->
<template v-for="column in columns" :key="column.prop">
<el-table-column
:prop="column.prop"
:label="column.label"
:width="column.width"
:show-overflow-tooltip="column.showOverFlow"
:sortable="column.sortable ?? false"
:formatter="column.formatter"
>
<template #default="scope" v-if="column.slot">
<slot :name="column.prop" :row="scope.row" />
</template>
</el-table-column>
</template>
</el-table>
<!-- 分页 -->
<div class="table-footer" v-if="showPagination">
<CustomPagination
:total="totalCount"
:current-page="currentPage"
:page-size="pageSize"
:page-sizes="pageSizes"
@update:current-page="handleCurrentChange"
@update:page-size="handleSizeChange"
@pagination="(params) => emit('pagination', params)"
/>
</div>
<el-button type="primary" @click="printSelected">打印已选择数据</el-button>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch, nextTick } from 'vue';
import cloneDeep from 'lodash/cloneDeep';
import { findIndex } from 'lodash';
import CustomPagination from './CustomPagination.vue';
interface Props {
data: TableRow[];
columns: {
prop: string;
label: string;
width?: number | string;
sortable?: boolean;
showOverFlow?: boolean;
formatter?: (row: TableRow) => string;
slot?: boolean;
}[];
showSelection?: boolean;
showIndex?: boolean;
showPagination?: boolean;
stripe?: boolean;
border?: boolean;
pageSizes?: number[];
defaultPageSize?: number;
rowSelectable?: (row: TableRow) => boolean;
total?: number;
}
const props = withDefaults(defineProps<Props>(), {
showSelection: false,
showIndex: false,
showPagination: true,
border: true,
stripe: true,
pageSizes: () => [10, 20, 50, 100],
defaultPageSize: 10,
rowSelectable: () => true,
total: 0,
});
// 定义事件
const emit = defineEmits<{
(e: 'update:pageSize', size: number): void;
(e: 'update:currentPage', page: number): void;
(e: 'pagination', params: { page: number; size: number }): void;
}>();
// 分页相关
const currentPage = ref(1);
const pageSize = ref(props.defaultPageSize);
const totalCount = computed(() => props.total);
// 表格对象的引用
const tableRef = ref();
// 当前页数据
const currentPageData = computed(() => {
if (!props.showPagination) return props.data;
return props.data;
});
// 定义数据行的接口
interface TableRow {
id: string | number;
[key: string]: any;
}
// 所有选中数据的存储
const selectedData = ref<TableRow[]>([]);
// 当前页选中数据的存储
const currentPageSelectedData = ref<TableRow[]>([]);
// 处理选择变化
const handleSelectionChange = (selection: TableRow[]) => {
console.log(currentPageSelectedData.value, selection);
selection.forEach((row: TableRow) => {
if (!currentPageSelectedData.value.includes(row)) {
currentPageSelectedData.value.push(row);
}
});
console.log(currentPageSelectedData.value, 'currentPageSelectedData.value');
const oldCurrentSelected = cloneDeep(currentPageSelectedData.value);
currentPageSelectedData.value = currentPageSelectedData.value.filter((item: TableRow) =>
selection.includes(item)
);
let tempData = cloneDeep(selectedData.value);
if (selection.length > 0) {
currentPageData.value.forEach((item: TableRow) => {
const index = findIndex(tempData, (row: TableRow) => item.id === row.id);
if (index > -1) {
// 确保插入最终数据中的值不会出现重复的
tempData.splice(index, 1);
}
});
} else if (selection.length == 0) {
// 当前页全部取消了勾选
oldCurrentSelected.forEach((item: TableRow) => {
const findRes = currentPageSelectedData.value.find((item: TableRow) => item.id === item.id);
if (!findRes) {
tempData = tempData.filter((tempItem: TableRow) => tempItem.id !== item.id);
}
});
}
selectedData.value = tempData.concat(selection);
console.log(selectedData.value, 'selectedData.value');
};
// 分页处理
const handleSizeChange = (val: number) => {
pageSize.value = val;
currentPage.value = 1;
emit('update:pageSize', val);
emit('update:currentPage', 1);
emit('pagination', { page: 1, size: val });
};
const handleCurrentChange = (val: number) => {
currentPage.value = val;
emit('update:currentPage', val);
emit('pagination', { page: val, size: pageSize.value });
};
// 处理接口返回的数据
const handleData = () => {
console.log('重新处理数据');
currentPageSelectedData.value = []; // 清空当前页选中的数据
tableRef.value?.clearSelection();
console.log(currentPageData.value, selectedData.value);
// 使用nextTick来解决当数据返回延迟的问题
nextTick(() => {
if (tableRef.value && currentPageData.value) {
currentPageData.value.forEach((item) => {
const findRes = selectedData.value.find((selectedItem) => selectedItem.id === item.id);
if (findRes) {
console.log('找到数据', findRes);
tableRef.value?.toggleRowSelection(item, true);
currentPageSelectedData.value.push(item);
}
});
}
});
};
// 打印选中数据
const printSelected = () => {
console.log('当前选中的所有数据:', selectedData.value);
};
// 监听当前页数据变化
watch(
() => currentPageData.value,
() => {
handleData();
},
{ deep: true } // 添加 deep: true 以确保能监听到数组内部的变化
);
// 获取行的唯一键
const getRowKey = (row: TableRow) => {
return row.id || Math.random().toString(36).substr(2, 9);
};
</script>
<style scoped>
.custom-table-container {
width: 100%;
}
.table-footer {
margin-top: 20px;
display: flex;
justify-content: flex-end;
align-items: center;
}
</style>
示例demo
<template>
<div class="table-demo">
<custom-table
:data="tableData"
:columns="columns"
:show-pagination="true"
:show-selection="true"
:border="false"
:page-sizes="[5, 10, 20, 50]"
:default-page-size="5"
:total="total"
v-model:current-page="currentPage"
v-model:page-size="pageSize"
@pagination="handlePagination"
>
<!-- 自定义操作列插槽 -->
<template #operation="{ row }">
<el-button type="primary" size="small" @click="handleEdit(row)">编辑</el-button>
<el-button type="danger" size="small" @click="handleDelete(row)">删除</el-button>
</template>
</custom-table>
</div>
</template>
<script setup lang="ts">
import CustomTable from '../components/CustomTable.vue';
import { ref } from 'vue';
// 分页参数
const currentPage = ref(1);
const pageSize = ref(5);
const total = ref(100); // 总数据量
// 表格数据
const tableData = ref([]);
// 模拟获取数据的函数
const fetchData = async (page: number, size: number) => {
// 这里模拟后端接口调用
const start = (page - 1) * size;
const end = start + size;
// 模拟异步请求
const response = await new Promise((resolve) => {
setTimeout(() => {
resolve({
list: Array.from({ length: size }, (_, index) => ({
id: start + index + 1,
name: `用户${start + index + 1}`,
age: Math.floor(Math.random() * 50) + 18,
address: `测试地址测试地址测试地址测试地址测试地址测试地址测试地址测试地址${start + index + 1}`,
date: new Date(Date.now() - Math.random() * 10000000000).toLocaleDateString(),
})),
total: 100,
});
}, 500);
});
return response;
};
// 处理分页变化
const handlePagination = async ({ page, size }: { page: number; size: number }) => {
const res = await fetchData(page, size);
tableData.value = res.list;
total.value = res.total;
};
// 初始化加载数据
handlePagination({ page: currentPage.value, size: pageSize.value });
// 列配置
const columns = [
{
prop: 'name',
label: '姓名',
sortable: false,
},
{
prop: 'age',
label: '年龄',
sortable: true,
},
{
prop: 'address',
label: '地址',
width: 300,
showOverFlow: true,
},
{
prop: 'date',
label: '日期',
sortable: true,
},
{
prop: 'operation',
label: '操作',
slot: true,
width: 200,
},
];
const handleEdit = (row: any) => {
console.log('编辑行:', row);
};
const handleDelete = (row: any) => {
console.log('删除行:', row);
};
</script>
<style scoped>
.table-demo {
width: 100%;
min-width: 1000px;
padding: 20px;
}
</style>
小结
通过上面的代码,就可以完美的实现用户点击分页后依旧能够保留点击的数据的场景,值得注意的是,小鹿在项目中额外用到了cloneDeep、 findIndex 这两个库方法,他们都属于lodash库中的方法,这里需要你们自行在项目中安装,这两个方法对版本要求不高,因此不论是内网开发还是外网开发,应该都可以满足。