vue2 el-table使用
loading
1、table基础表格渲染(动态表头数据,排序)
<template> <div class="table-container"> <!-- 动态表头表格 --> <el-table :data="tableData" border fit stripe @sort-change="handleChangeSort" > <!-- 循环渲染动态表头 --> <template v-for="(item, index) in dynamicColumns"> <el-table-column v-if="item.isShow" :key="item.prop + index" :prop="item.prop" :label="item.label" :width="item.width" :show-overflow-tooltip="true" :align="item.align || 'center'" :sortable="item.sortable" :minWidth="item.minWidth" :formatter="columnFormatter" > <!-- 可选:自定义列内容(比如格式化、按钮等) --> <template slot-scope="scope"> <!-- 示例:如果是操作列,渲染按钮 --> <template v-if="item.prop === 'operation'"> <el-button type="primary" size="mini" @click="handleEdit(scope.row)" >编辑</el-button > <el-button type="danger" size="mini" @click="handleDelete(scope.row)" >删除</el-button > </template> <!-- 普通列直接显示值 --> <template v-else> {{ scope.row[item.prop] }} </template> </template> </el-table-column> </template> </el-table> </div> </template> <script> export default { name: "DynamicTable", data() { return { // 表格数据源 tableData: [ { id: 1, name: "张三", age: 20, phone: "13800138000" }, { id: 2, name: "李四", age: 22, phone: "13900139000" }, { id: 3, name: "王五", age: 25, phone: "13700137000" }, ], // 动态表头配置(核心) dynamicColumns: [ { prop: "id", label: "ID", width: "80px", sortable: true, isShow: true, align: "center", minWidth: 500, }, { prop: "name", label: "姓名", sortable: true, isShow: this.$route.name === "TestTest", width: "120px", }, { prop: "age", label: "年龄", sortable: true, isShow: true, width: "80px", }, { prop: "phone", label: "手机号", sortable: true, isShow: true, width: "180px", }, { prop: "operation", label: "操作", isShow: true, width: "180px", }, ], }; }, mounted() { console.log(this.$route.name); // 隐藏id - 通过请求接口类型控制 const obj = this.getColumn("id"); obj.isShow = false; }, methods: { // 获取指定列的配置 getColumn(prop) { return this.dynamicColumns.find((col) => col.prop === prop); }, getSort(column) { const { order, prop } = column; let sortType = ""; if (order === "ascending") { sortType = order.slice(0, 3); } else if (order === "descending") { sortType = order.slice(0, 4); } return { sortCol: sortType ? prop : "", sortType: sortType, }; }, handleChangeSort(column) { const {sortCol, sortType} = this.getSort(column) console.log(sortCol, sortType) // 请求接口 }, // 字段值转换 columnFormatter(row, column, cellValue, index) { if (item.property === "systemSts") { return "转码"; // return this.selectDictLabel(this.systemStsArr, row.systemSts) } else { return cellValue; } }, // 编辑操作 handleEdit(row) { console.log("编辑:", row); this.$message.success(`准备编辑 ${row.name}`); }, // 删除操作 handleDelete(row) { console.log("删除:", row); this.$message.warning(`准备删除 ${row.name}`); }, }, }; </script> <style scoped> .table-container { padding: 20px; } </style>
2、table基础封装 通过父子组件、slot、多选功能

父组件部分 fatherTable.vue
<template> <div class="table-page"> <!-- 操作按钮:配合复选框使用 --> <div class="table-toolbar" style="margin-bottom: 10px"> <el-button type="primary" @click="handleGetSelected">获取选中行</el-button> <el-button type="warning" @click="handleClearSelected">清空选中</el-button> <el-button type="info" @click="handleToggleFirstRow">选中第一行</el-button> </div> <!-- 使用封装的动态表格 --> <DynamicTable ref="dynamicTable" :table-data="tableData" :columns="columns" :tableConfig="tableConfig" border :loading="loading" @selection-change="handleSelectionChange" @row-click="handleRowClick" > <!-- 动态插槽:对应columns中slotName为operation的列 --> <template #operation="{ row, index}"> <el-button type="primary" size="mini" @click="handleEdit(row)">编辑</el-button> <el-button type="danger" size="mini" @click="handleDelete(row, index)">删除</el-button> </template> <!-- 扩展:可自定义其他列的插槽(比如姓名列加样式) --> <template #name="{ row }"> <span :style="{ color: row.age > 20 ? 'red' : 'black' }"> {{ row.name }}({{ row.age }}岁) </span> </template> </DynamicTable> </div> </template> <script> import DynamicTable from "./DynamicTable.vue"; // 引入封装的表格组件 export default { name: "TablePage", components: { DynamicTable }, data() { return { loading: false, selectedRows: [], // 表格数据 tableData: [ { id: 1, name: "张三", age: 22, phone: "13800138000" }, { id: 2, name: "李四", age: 19, phone: "13900139000" }, { id: 3, name: "王五", age: 25, phone: "13700137000" } ], // 动态表头配置(指定插槽名) columns: [ { prop: "id", label: "ID", width: "80px" }, { prop: "name", label: "姓名", width: "150px", slotName: "name" }, // 自定义姓名列插槽 { prop: "age", label: "年龄", width: "80px" }, { prop: "phone", label: "手机号", width: "180px" }, { prop: "operation", label: "操作", width: "200px", slotName: "operation" } // 操作列插槽 ], // 配置项 tableConfig:{ selection: true, selectionWidth:'55', selectionFixed: true, talbeIndex: false } }; }, methods: { // 监听选中行变化 handleSelectionChange(val) { this.selectedRows = val; console.log("当前选中行:", val); }, // 获取选中行 handleGetSelected() { // const selected = this.$refs.dynamicTable.getSelectedRows(); const selected = this.selectedRows this.$message.success(`选中了 ${selected.length} 行数据`); console.log("获取选中行:", selected); }, // 清空选中行 handleClearSelected() { this.$refs.dynamicTable.clearSelection(); this.selectedRows = []; this.$message.warning("已清空所有选中行"); }, // 切换第一行的选中状态 handleToggleFirstRow() { if (this.tableData.length > 0) { this.$refs.dynamicTable.toggleSelection(this.tableData[0]); } }, // 行点击事件 handleRowClick(row) { console.log("点击行:", row); // this.$refs.dynamicTable.toggleSelection(row); }, // 编辑 handleEdit(row) { this.$message.success(`编辑${row.name}`); }, // 删除 handleDelete(row, index) { this.$confirm("确定删除?", "提示", { type: "warning" }).then(() => { this.tableData.splice(index, 1); this.$message.success("删除成功"); }); } } }; </script>
自组件 DynamicTable.vue
<template> <el-table ref="dynamicTableRef" :data="tableData" v-bind="$attrs" v-on="$listeners" style="width: 100%" > <!-- 1. 复选框列:配置showSelection为true时渲染 --> <el-table-column v-if="tableConfig.selection" type="selection" align="center" :width="tableConfig.selectionWidth || '55px'" :fixed="tableConfig.selectionFixed || true" /> <el-table-column v-if="tableConfig.talbeIndex" type="index" label="序号" align="center" width="50"/> <!-- 循环渲染动态表头 --> <el-table-column v-for="(column, index) in columns" :key="column.prop || index" :prop="column.prop" :label="column.label" :width="column.width" :align="column.align || 'center'" :fixed="column.fixed" > <!-- 动态插槽:如果配置了slotName,则使用对应插槽渲染 --> <template slot-scope="scope"> <!-- 优先使用自定义插槽 --> <slot v-if="column.slotName" :name="column.slotName" :row="scope.row" :index="scope.$index" :column="column" /> <!-- 无插槽时默认渲染字段值 --> <span v-else> {{ scope.row[column.prop] || "-" }} </span> </template> </el-table-column> </el-table> </template> <script> export default { name: "DynamicTable", props: { tableConfig:{ type:Object, default:()=>({ selection: false, selectionWidth:'55', selectionFixed: true, talbeIndex: false }) }, // 表格数据源(必传) tableData: { type: Array, required: true, default: () => [], }, // 表头配置数组(必传) columns: { type: Array, required: true, default: () => [], /* 配置项示例: [ { prop: 'id', label: 'ID', width: '80px' }, // 无插槽,默认渲染 { prop: 'name', label: '姓名', width: '120px' }, { prop: 'operation', label: '操作', width: '200px', slotName: 'operation' } // 自定义插槽 ] */ }, }, inheritAttrs: false, // 关闭属性继承,避免根元素继承非props属性 methods: { // 暴露给父组件的方法:获取选中行 getSelectedRows() { return this.$refs.dynamicTableRef?.getSelectionRows() || []; }, // 暴露给父组件的方法:清空选中行 clearSelection() { this.$refs.dynamicTableRef?.clearSelection(); }, // 暴露给父组件的方法:切换指定行的选中状态 toggleSelection(row) { this.$refs.dynamicTableRef?.toggleRowSelection(row); }, }, }; </script>
3、table基础封装\展开行\树形数据、懒加载

展开行 -通过配置项设置
// 配置项 tableConfig: { selection: false, selectionWidth: "55", selectionFixed: true, talbeIndex: false, isExpand: true, // 展开行 expandSlotName:'expand' },
属性设置
<!--type=expand 展开行-->
<template v-if="tableConfig.isExpand">
<el-table-column type="expand">
<template slot-scope="scope">
<slot
:name="tableConfig.expandSlotName"
:row="scope.row"
:index="scope.$index"
/>
</template>
</el-table-column>
</template>
页面插槽部分
<!-- expand 插槽 -->
<template #expand="{ row, index }">
{{ index }} -{{row}}
</template>
完整部分 -树形懒加载部分
父组件。index.vue
<template> <div class="table-page"> <!-- 操作按钮:配合复选框使用 --> <div class="table-toolbar" style="margin-bottom: 10px"> <el-button type="primary" @click="handleGetSelected" >获取选中行</el-button > <el-button type="warning" @click="handleClearSelected" >清空选中</el-button > <el-button type="info" @click="handleToggleFirstRow" >选中第一行</el-button > </div> <!-- 使用封装的动态表格 --> <DynamicTable ref="dynamicTable" :table-data="tableData" :columns="columns" :tableConfig="tableConfig" row-key="id" :tree-props="{ children: 'children', hasChildren: 'hasChildren' }" lazy :load="load" border @selection-change="handleSelectionChange" @row-click="handleRowClick" @sort-change="sortChange" > <!-- 动态插槽:对应columns中slotName为operation的列 --> <template #operation="{ row, index }"> <el-button type="primary" size="mini" @click="handleEdit(row)" >编辑</el-button > <el-button type="danger" size="mini" @click="handleDelete(row, index)" >删除</el-button > </template> <!-- 扩展:可自定义其他列的插槽(比如姓名列加样式) --> <template #name="{ row }"> <span :style="{ color: row.age > 20 ? 'red' : 'black' }"> {{ row.name }}({{ row.age }}岁) </span> </template> <!-- expand 插槽 --> <template #expand="{ row, index }"> {{ index }} -{{row}} </template> </DynamicTable> </div> </template> <script> import DynamicTable from "./DynamicTable.vue"; // 引入封装的表格组件 export default { name: "TablePage", components: { DynamicTable }, data() { return { loading: false, selectedRows: [], // 表格数据 tableData: [ { id: 1, name: "张三", age: 22, phone: "13800138000", hasChildren: true // children: [ // { // id: 31, // age: 32, // phone: "12800138000", // name: "王小虎", // children:[] // }, // { // id: 32, // age: 33, // phone: "14800138000", // name: "王小虎", // children:[] // }, // ], }, { id: 2, name: "李四", age: 19, phone: "13900139000" ,hasChildren: true}, { id: 3, name: "王五", age: 25, phone: "13700137000" }, ], // 配置项 tableConfig: { selection: false, selectionWidth: "55", selectionFixed: true, talbeIndex: false, isExpand: true, // 展开行 expandSlotName:'expand' }, // 动态表头配置(指定插槽名) columns: [ { prop: "id", label: "ID", width: "180px" }, { prop: "name", label: "姓名", width: "150px", sortable: true, slotName: "name", }, // 自定义姓名列插槽 { prop: "age", label: "年龄", width: "180px" }, { prop: "phone", label: "手机号", width: "180px" }, { prop: "operation", label: "操作", width: "200px", slotName: "operation", }, // 操作列插槽 ], }; }, methods: { load(tree, treeNode, resolve) { console.log('treeNode',treeNode) const testData = [ { id: 31, age: 32, phone: "12800138000", name: "王小虎", children:[] }, ] resolve(testData) }, // 监听选中行变化 handleSelectionChange(val) { this.selectedRows = val; console.log("当前选中行:", val); }, // 获取选中行 handleGetSelected() { // const selected = this.$refs.dynamicTable.getSelectedRows(); const selected = this.selectedRows; this.$message.success(`选中了 ${selected.length} 行数据`); console.log("获取选中行:", selected); }, // 清空选中行 handleClearSelected() { this.$refs.dynamicTable.clearSelection(); this.selectedRows = []; this.$message.warning("已清空所有选中行"); }, // 切换第一行的选中状态 handleToggleFirstRow() { if (this.tableData.length > 0) { this.$refs.dynamicTable.toggleSelection(this.tableData[0]); } }, // 行点击事件 handleRowClick(row) { console.log("点击行:", row); // this.$refs.dynamicTable.toggleSelection(row); }, // 编辑 handleEdit(row) { this.$message.success(`编辑${row.name}`); }, // 删除 handleDelete(row, index) { this.$confirm("确定删除?", "提示", { type: "warning" }).then(() => { this.tableData.splice(index, 1); this.$message.success("删除成功"); }); }, sortChange(column) { const { prop, order } = column; // 分割 slice console.log(column); }, }, }; </script> <style scoped> /* 父组件样式 */ .el-table { --el-table-row-height: 40px; /* 行高足够 */ } /* 确保树形箭头显示 */ .el-table__expand-icon { display: block !important; margin-right: 5px; } /* 复选框列宽度足够,避免遮挡箭头 */ .el-table-column--selection { width: 60px !important; } </style>
子组件部分 DynamicTable
<!-- 1、 tableData 表格数据 2、 columns 字段数据 3、 tableConfig { 表格配置项 selection 是否显示复选框 默认 false talbeIndex 表格序列号 selectionWidth selectionFixed isExpand: false, expandSlotName 指定名字 row-key 必须id, :tree-props={children: 'children'} 渲染树形数据时 } 4、树形数据 --> <template> <el-table ref="dynamicTableRef" :data="tableData" v-bind="$attrs" v-on="$listeners" style="width: 100%" > <!--type=expand 展开行--> <template v-if="tableConfig.isExpand"> <el-table-column type="expand"> <template slot-scope="scope"> <slot :name="tableConfig.expandSlotName" :row="scope.row" :index="scope.$index" /> </template> </el-table-column> </template> <!-- 1. 复选框列:配置showSelection为true时渲染 --> <el-table-column v-if="tableConfig.selection" type="selection" align="center" :width="tableConfig.selectionWidth || '55px'" :fixed="tableConfig.selectionFixed || true" /> <el-table-column v-if="tableConfig.talbeIndex" type="index" label="序号" align="center" width="50" /> <!-- type="expand" --> <!-- 循环渲染动态表头 --> <el-table-column v-for="(column, index) in columns" :key="column.prop || index" :prop="column.prop" :label="column.label" :width="column.width" :align="column.align || 'center'" :sortable="column.sortable" :fixed="column.fixed" > <!-- 动态插槽:如果配置了slotName,则使用对应插槽渲染 --> <template slot-scope="scope"> <!-- 优先使用自定义插槽 --> <slot v-if="column.slotName" :name="column.slotName" :row="scope.row" :index="scope.$index" :column="column" /> <!-- 无插槽时默认渲染字段值 --> <span v-else> {{ scope.row[column.prop] || "-" }} </span> </template> </el-table-column> </el-table> </template> <script> export default { name: "DynamicTable", props: { tableConfig: { type: Object, default: () => ({ selection: false, selectionWidth: "55", selectionFixed: true, talbeIndex: false, isExpand: false, expandSlotName: "", }), }, // 表格数据源(必传) tableData: { type: Array, required: true, default: () => [], }, // 表头配置数组(必传) columns: { type: Array, required: true, default: () => [], /* 配置项示例: [ { prop: 'id', label: 'ID', width: '80px' }, // 无插槽,默认渲染 { prop: 'name', label: '姓名', width: '120px' }, { prop: 'operation', label: '操作', width: '200px', slotName: 'operation' } // 自定义插槽 ] */ }, }, inheritAttrs: false, // 关闭属性继承,避免根元素继承非props属性 methods: { // 暴露给父组件的方法:获取选中行 getSelectedRows() { return this.$refs.dynamicTableRef?.getSelectionRows() || []; }, // 暴露给父组件的方法:清空选中行 clearSelection() { this.$refs.dynamicTableRef?.clearSelection(); }, // 暴露给父组件的方法:切换指定行的选中状态 toggleSelection(row) { this.$refs.dynamicTableRef?.toggleRowSelection(row); }, }, }; </script>
4、table基础封装\表格合并\根据单独字段合并\ 组合字段合并

数据如下:

完整示例:
index.vue
<template> <div class="table-page"> <!-- 操作按钮:配合复选框使用 --> <div class="table-toolbar" style="margin-bottom: 10px"> <el-button type="primary" @click="handleGetSelected" >获取选中行</el-button > <el-button type="warning" @click="handleClearSelected" >清空选中</el-button > <el-button type="info" @click="handleToggleFirstRow" >选中第一行</el-button > </div> <!-- 使用封装的动态表格 --> <DynamicTable ref="dynamicTable" :table-data="tableData" :columns="columns" :tableConfig="tableConfig" row-key="id" :tree-props="{ children: 'children', hasChildren: 'hasChildren' }" lazy :load="load" border @selection-change="handleSelectionChange" @row-click="handleRowClick" @sort-change="sortChange" :span-method="cellSpanMethod" > <!-- 动态插槽:对应columns中slotName为operation的列 --> <template #operation="{ row, index }"> <el-button type="primary" size="mini" @click="handleEdit(row)" >编辑</el-button > <el-button type="danger" size="mini" @click="handleDelete(row, index)" >删除</el-button > </template> <!-- 扩展:可自定义其他列的插槽(比如姓名列加样式) --> <template #name="{ row }"> <span :style="{ color: row.age > 20 ? 'red' : 'black' }"> {{ row.name }}({{ row.age }}岁) </span> </template> <!-- expand 插槽 --> <template #expand="{ row, index }"> {{ index }} - {{ JSON.stringify(row) }} </template> </DynamicTable> </div> </template> <script> import DynamicTable from "./DynamicTable.vue"; // 引入封装的表格组件 export default { name: "TablePage", components: { DynamicTable }, data() { return { loading: false, selectedRows: [], // 1. 定义合并规则:key=列prop,value=分组字段数组(单字段=单独合并,多字段=父级别合并) mergeRules: { id: ["id"], // 单独字段合并:仅按id name: ["name"], // 单独字段合并:仅按name age: ["name", "age"], // 父级别合并:按name+age组合 phone:["parentd","phone"] // 父级别合并:按parentd+ phone组合 }, mergeObjCell: { // 2. 提前初始化合并列的键,避免清空逻辑跳过 id: [], name: [], age: [], }, // 表格数据 tableData: [ { id: '0', parentd: 11, name: "张三", age: 22, phone: "13800138000" }, { id: 1, parentd: 11, name: "李四", age: 19, phone: "13800138000" }, { id: 3, parentd: 22, name: "王五", age: 26, phone: "13700137000" }, { id: 5, parentd: 22, name: "王五", age: 26, phone: "13700137000" }, { id: 6, parentd: 33, name: "王五1", age: 26, phone: "13700137000" }, { id: 7, parentd: 44, name: "王五1", age: 26, phone: "13700137000" }, ], // 配置项 tableConfig: { selection: true, selectionWidth: "55", selectionFixed: true, talbeIndex: false, isExpand: false, // 展开行 expandSlotName: "expand", }, // 动态表头配置(指定插槽名) columns: [ { prop: "id", label: "ID", width: "180px", slotHeader: true }, { prop: "name", label: "姓名", width: "150px", sortable: true, slotName: "name", }, // 自定义姓名列插槽 { prop: "age", label: "年龄", width: "180px" }, { prop: "phone", label: "手机号", width: "180px" }, { prop: "operation", label: "操作", width: "200px", slotName: "operation", }, // 操作列插槽 ], }; }, mounted() { // 数据处理:传入合并规则(支持单字段/父级别) this.getSpanArrCommon(this.tableData, this.mergeRules, this.mergeObjCell); console.log("合并配置:", this.mergeObjCell); // 验证配置是否生成 }, methods: { // 表格合并方法(修正参数取值 + 返回值格式) cellSpanMethod({ row, column, rowIndex, columnIndex }) { const colProp = column.property; // 仅处理配置了合并规则的列 if (this.mergeObjCell[colProp] !== undefined) { const rowspan = this.mergeObjCell[colProp][rowIndex] || 0; return { rowspan: rowspan, // 合并行数(0=隐藏,>0=合并) colspan: 1, // 列不合并 }; } // 未配置的列:默认不合并 return { rowspan: 1, colspan: 1 }; }, /** * 通用合并计算方法(支持单字段/父级别组合合并) * @param {Array} data - 表格数据 * @param {Object} mergeRules - 合并规则 { 列prop: 分组字段数组 } * @param {Object} mergeObj - 输出的合并配置对象 */ getSpanArrCommon(data, mergeRules, mergeObj) { if (!data || !data.length || !mergeRules) { return; } // 清空旧的合并配置 Object.keys(mergeObj).forEach((key) => { mergeObj[key] = []; }); // 遍历每一列的合并规则 Object.entries(mergeRules).forEach(([colProp, groupFields]) => { let count = 0; // 合并起始行索引 mergeObj[colProp] = []; // 初始化当前列的合并数组 // 遍历每行数据计算合并数 data.forEach((item, index) => { if (index === 0) { // 第一行默认合并数为1 mergeObj[colProp].push(1); } else { // 生成分组标识(支持多字段组合) const getGroupKey = (row) => { return groupFields .map((field) => { const value = row[field]; return value === null || value === undefined ? "" : value; }) .join("|"); }; const currentKey = getGroupKey(item); const prevKey = getGroupKey(data[index - 1]); // 判断是否属于同一分组 if (currentKey === prevKey) { // 同一分组:起始行合并数+1,当前行标记为0 mergeObj[colProp][count] += 1; mergeObj[colProp].push(0); } else { // 不同分组:重置起始索引,当前行合并数为1 count = index; mergeObj[colProp].push(1); } } }); }); }, load(tree, treeNode, resolve) { console.log("treeNode", treeNode); const testData = [ { id: 31, age: 32, phone: "12800138000", name: "王小虎", children: [], }, ]; resolve(testData); }, // 监听选中行变化 handleSelectionChange(val) { this.selectedRows = val; console.log("当前选中行:", val); }, // 获取选中行 handleGetSelected() { const selected = this.selectedRows; this.$message.success(`选中了 ${selected.length} 行数据`); console.log("获取选中行:", selected); }, // 清空选中行 handleClearSelected() { this.$refs.dynamicTable.clearSelection(); this.selectedRows = []; this.$message.warning("已清空所有选中行"); }, // 切换第一行的选中状态 handleToggleFirstRow() { if (this.tableData.length > 0) { this.$refs.dynamicTable.toggleSelection(this.tableData[0]); } }, // 行点击事件 handleRowClick(row) { console.log("点击行:", row); }, // 编辑 handleEdit(row) { this.$message.success(`编辑${row.name}`); }, // 删除 handleDelete(row, index) { this.$confirm("确定删除?", "提示", { type: "warning" }).then(() => { this.tableData.splice(index, 1); // 删除后重新计算合并配置 this.getSpanArrCommon( this.tableData, this.mergeRules, this.mergeObjCell, ); this.$message.success("删除成功"); }); }, sortChange(column) { const { prop, order } = column; console.log(column); // 排序后重新计算合并配置 this.getSpanArrCommon(this.tableData, this.mergeRules, this.mergeObjCell); }, }, }; </script> <style scoped> /* 父组件样式 */ .el-table { --el-table-row-height: 40px; /* 行高足够 */ } /* 确保树形箭头显示 */ .el-table__expand-icon { display: block !important; margin-right: 5px; } /* 复选框列宽度足够,避免遮挡箭头 */ .el-table-column--selection { width: 60px !important; } </style>
DynamicTable.vue (无变化)
5、el-table 大数据展示卡顿问题
方案1 slice解决
async loadMoreData() { // 避免重复加载(前置判断,快速返回) if (this.loading || this.noMore) return; this.loading = true; try { // 实际接口 await this.mockApiRequest(); // 计算数据索引(优化:边界处理,避免超出数组长度) const start = (this.currentPage - 1) * this.pageSize; const end = Math.min(start + this.pageSize, this.originData.length); const newData = this.originData.slice(start, end); this.tableData = [...this.tableData, ...newData]; // 合并单元格 getSpanArrCommon([...this.tableData],this.mergeRules,this.mergeObjCell); // 更新分页状态 this.currentPage += 1; // 判断是否加载完毕(优化:精准判断,避免无效加载) this.noMore = end >= this.originData.length; } finally { // 无论成功失败,都解除加载锁(优化:避免加载状态卡死) this.loading = false; }
监听加载滚动 ---el-table滚动 这里是设置el-table 高度 表头固定
/ 组件挂载后,获取内部滚动容器 this.scrollContainer = this.$refs.dynamicTable.$refs.dynamicTableRef.$el.querySelector( ".el-table__body-wrapper", ); // 绑定原生 scroll 事件 if (this.scrollContainer) { this.scrollContainer.addEventListener("scroll", this.handleTableScroll); }
滚动加载更多
handleTableScroll: _.throttle(function (scrollInfo) { const { scrollTop, scrollHeight, clientHeight } = scrollInfo.target; console.log(scrollTop, scrollHeight, clientHeight); // 滚动距离底部50px时触发加载(优化:增加边界判断,避免无效触发) const isNearBottom = scrollTop + clientHeight >= scrollHeight - 500; // 仅当接近底部、未加载中、还有数据时,才触发加载 if (isNearBottom && !this.loading && !this.noMore) { console.log("scrollInfo", scrollInfo); this.loadMoreData(); } }, 500),
完整代码--根据实际是否需要合并
index.vue
<template> <div class="table-page"> <!-- 操作按钮:配合复选框使用 --> <div class="table-toolbar" style="margin-bottom: 10px"> <el-button type="primary" @click="handleGetSelected" >获取选中行</el-button > <el-button type="warning" @click="handleClearSelected" >清空选中</el-button > <el-button type="info" @click="handleToggleFirstRow" >选中第一行</el-button > </div> <!-- 使用封装的动态表格 --> <DynamicTable ref="dynamicTable" :table-data="tableData" :columns="columns" :tableConfig="tableConfig" row-key="id" :tree-props="{ children: 'children', hasChildren: 'hasChildren' }" lazy :load="load" border height="90vh" @selection-change="handleSelectionChange" @row-click="handleRowClick" @sort-change="sortChange" :span-method="cellSpanMethod" > <!-- 动态插槽:对应columns中slotName为operation的列 --> <template #operation="{ row, index }"> <el-button type="primary" size="mini" @click="handleEdit(row)" >编辑</el-button > <el-button type="danger" size="mini" @click="handleDelete(row, index)" >删除</el-button > </template> <!-- 扩展:可自定义其他列的插槽(比如姓名列加样式) --> <template #name="{ row }"> <span :style="{ color: row.age > 20 ? 'red' : 'black' }"> {{ row.name }}({{ row.age }}岁) </span> </template> <!-- expand 插槽 --> <template #expand="{ row, index }"> {{ index }} - {{ JSON.stringify(row) }} </template> </DynamicTable> </div> </template> <script> // 引入 lodash 节流(若项目未集成,可使用自定义节流函数) import _ from "lodash"; import DynamicTable from "./DynamicTable.vue"; // 引入封装的表格组件 import { getSpanArrCommon } from "./utils.js"; export default { name: "TablePage", components: { DynamicTable }, data() { return { originData: [], // 大数据量存储 tableData: [], // 当前页面表格渲染 pageSize: 10, // 每次加载100条(可根据性能调整,建议50-200) currentPage: 1, // 当前加载到第几页 loading: false, // 是否正在加载 noMore: false, // 是否已加载全部数据 loading: false, selectedRows: [], // 1. 定义合并规则:key=列prop,value=分组字段数组(单字段=单独合并,多字段=父级别合并) mergeRules: { id: ["id"], // 单独字段合并:仅按id name: ["name"], // 单独字段合并:仅按name age: ["name", "age"], // 父级别合并:按name+age组合 phone: ["parentd", "phone"], // 父级别合并:按parentd+ phone组合 }, mergeObjCell: { // 2. 提前初始化合并列的键,避免清空逻辑跳过 id: [], name: [], age: [], }, // 表格数据 tableData: [ { id: "0", parentd: 11, name: "张三", age: 22, phone: "13800138000" }, { id: 1, parentd: 11, name: "李四", age: 19, phone: "13800138000" }, { id: 3, parentd: 22, name: "王五", age: 26, phone: "13700137000" }, { id: 5, parentd: 22, name: "王五", age: 26, phone: "13700137000" }, { id: 6, parentd: 33, name: "王五1", age: 26, phone: "13700137000" }, { id: 7, parentd: 44, name: "王五1", age: 26, phone: "13700137000" }, ], // 配置项 tableConfig: { selection: true, selectionWidth: "55", selectionFixed: true, talbeIndex: false, isExpand: false, // 展开行 expandSlotName: "expand", }, // 动态表头配置(指定插槽名) columns: [ { prop: "id", label: "ID", width: "180px", slotHeader: true }, { prop: "name", label: "姓名", width: "150px", sortable: true, slotName: "name", }, // 自定义姓名列插槽 { prop: "age", label: "年龄", width: "180px" }, { prop: "phone", label: "手机号", width: "180px" }, { prop: "operation", label: "操作", width: "200px", slotName: "operation", }, // 操作列插槽 ], }; }, mounted() { // test let arrTmep = []; for (let i = 0; i < 100; i++) { arrTmep.push({ id: 100 + i, parentd: 44, name: "王五1", age: 26, phone: "13700137000", }); } this.originData = [...arrTmep]; this.loadMoreData(); // 组件挂载后,获取内部滚动容器 this.scrollContainer = this.$refs.dynamicTable.$refs.dynamicTableRef.$el.querySelector( ".el-table__body-wrapper", ); // 绑定原生 scroll 事件 if (this.scrollContainer) { this.scrollContainer.addEventListener("scroll", this.handleTableScroll); } // this.tableData = [...this.tableData, ...arrTmep] // console.log('arrTmep',arrTmep) // 数据处理:传入合并规则(支持单字段/父级别) // getSpanArrCommon([...this.tableData, ...arrTmep], this.mergeRules, this.mergeObjCell); // console.log("合并配置:", this.mergeObjCell); // 验证配置是否生成 }, methods: { handleTableScroll: _.throttle(function (scrollInfo) { const { scrollTop, scrollHeight, clientHeight } = scrollInfo.target; console.log(scrollTop, scrollHeight, clientHeight); // 滚动距离底部50px时触发加载(优化:增加边界判断,避免无效触发) const isNearBottom = scrollTop + clientHeight >= scrollHeight - 500; // 仅当接近底部、未加载中、还有数据时,才触发加载 if (isNearBottom && !this.loading && !this.noMore) { console.log("scrollInfo", scrollInfo); this.loadMoreData(); } }, 500), async loadMoreData() { // 避免重复加载(前置判断,快速返回) if (this.loading || this.noMore) return; this.loading = true; try { // 实际接口 await this.mockApiRequest(); // 计算数据索引(优化:边界处理,避免超出数组长度) const start = (this.currentPage - 1) * this.pageSize; const end = Math.min(start + this.pageSize, this.originData.length); const newData = this.originData.slice(start, end); this.tableData = [...this.tableData, ...newData]; // 合并单元格 getSpanArrCommon([...this.tableData],this.mergeRules,this.mergeObjCell); // 更新分页状态 this.currentPage += 1; // 判断是否加载完毕(优化:精准判断,避免无效加载) this.noMore = end >= this.originData.length; } finally { // 无论成功失败,都解除加载锁(优化:避免加载状态卡死) this.loading = false; } }, // 表格合并方法(修正参数取值 + 返回值格式) cellSpanMethod({ row, column, rowIndex, columnIndex }) { const colProp = column.property; // 仅处理配置了合并规则的列 if (this.mergeObjCell[colProp] !== undefined) { const rowspan = this.mergeObjCell[colProp][rowIndex] || 0; return { rowspan: rowspan, // 合并行数(0=隐藏,>0=合并)···· colspan: 1, // 列不合并· }; } // 未配置的列:默认不合并 return { rowspan: 1, colspan: 1 }; }, mockApiRequest() { return new Promise((resolve) => { setTimeout(resolve, 300); // 缩短延迟时间,提升用户体验 }); }, /** * 通用合并计算方法(支持单字段/父级别组合合并) * @param {Array} data - 表格数据 * @param {Object} mergeRules - 合并规则 { 列prop: 分组字段数组 } * @param {Object} mergeObj - 输出的合并配置对象 */ getSpanArrCommon(data, mergeRules, mergeObj) { if (!data || !data.length || !mergeRules) { return; } // 清空旧的合并配置 Object.keys(mergeObj).forEach((key) => { mergeObj[key] = []; }); // 遍历每一列的合并规则 Object.entries(mergeRules).forEach(([colProp, groupFields]) => { let count = 0; // 合并起始行索引 mergeObj[colProp] = []; // 初始化当前列的合并数组 // 遍历每行数据计算合并数 data.forEach((item, index) => { if (index === 0) { // 第一行默认合并数为1 mergeObj[colProp].push(1); } else { // 生成分组标识(支持多字段组合) const getGroupKey = (row) => { return groupFields .map((field) => { const value = row[field]; return value === null || value === undefined ? "" : value; }) .join("|"); }; const currentKey = getGroupKey(item); const prevKey = getGroupKey(data[index - 1]); // 判断是否属于同一分组 if (currentKey === prevKey) { // 同一分组:起始行合并数+1,当前行标记为0 mergeObj[colProp][count] += 1; mergeObj[colProp].push(0); } else { // 不同分组:重置起始索引,当前行合并数为1 count = index; mergeObj[colProp].push(1); } } }); }); }, load(tree, treeNode, resolve) { console.log("treeNode", treeNode); const testData = [ { id: 31, age: 32, phone: "12800138000", name: "王小虎", children: [], }, ]; resolve(testData); }, // 监听选中行变化 handleSelectionChange(val) { this.selectedRows = val; console.log("当前选中行:", val); }, // 获取选中行 handleGetSelected() { const selected = this.selectedRows; this.$message.success(`选中了 ${selected.length} 行数据`); console.log("获取选中行:", selected); }, // 清空选中行 handleClearSelected() { this.$refs.dynamicTable.clearSelection(); this.selectedRows = []; this.$message.warning("已清空所有选中行"); }, // 切换第一行的选中状态 handleToggleFirstRow() { if (this.tableData.length > 0) { this.$refs.dynamicTable.toggleSelection(this.tableData[0]); } }, // 行点击事件 handleRowClick(row) { console.log("点击行:", row); }, // 编辑 handleEdit(row) { this.$message.success(`编辑${row.name}`); }, // 删除 handleDelete(row, index) { this.$confirm("确定删除?", "提示", { type: "warning" }).then(() => { this.tableData.splice(index, 1); // 删除后重新计算合并配置 getSpanArrCommon(this.tableData, this.mergeRules, this.mergeObjCell); this.$message.success("删除成功"); }); }, sortChange(column) { const { prop, order } = column; console.log(column); // 排序后重新计算合并配置 getSpanArrCommon(this.tableData, this.mergeRules, this.mergeObjCell); }, }, }; </script> <style scoped> /* 父组件样式 */ .el-table { --el-table-row-height: 40px; /* 行高足够 */ } /* 确保树形箭头显示 */ .el-table__expand-icon { display: block !important; margin-right: 5px; } /* 复选框列宽度足够,避免遮挡箭头 */ .el-table-column--selection { width: 60px !important; } </style>
DynamicTable.vue (原有的)
<!-- 1、 tableData 表格数据 2、 columns 字段数据 3、 tableConfig { 表格配置项 selection 是否显示复选框 默认 false talbeIndex 表格序列号 selectionWidth selectionFixed isExpand: false, expandSlotName 指定名字 row-key 必须id, :tree-props={children: 'children'} 渲染树形数据时 headerSlot 自定义头部 } 4、树形数据 --> <template> <el-table ref="dynamicTableRef" :data="tableData" v-bind="$attrs" v-on="$listeners" style="width: 100%" > <!--type=expand 展开行--> <template v-if="tableConfig.isExpand"> <el-table-column type="expand"> <template slot-scope="scope"> <slot :name="tableConfig.expandSlotName" :row="scope.row" :index="scope.$index" /> </template> </el-table-column> </template> <!-- 1. 复选框列:配置showSelection为true时渲染 --> <el-table-column v-if="tableConfig.selection" type="selection" align="center" :width="tableConfig.selectionWidth || '55px'" :fixed="tableConfig.selectionFixed || true" /> <el-table-column v-if="tableConfig.talbeIndex" type="index" label="序号" align="center" width="50" /> <!-- type="expand" --> <!-- 循环渲染动态表头 --> <el-table-column v-for="(column, index) in columns" :key="column.prop || index" :prop="column.prop" :label="column.label" :width="column.width" :align="column.align || 'center'" :sortable="column.sortable" :fixed="column.fixed" > <!-- 自定义头部 --> <template slot="header" v-if="column.slotHeader"> 自定义ID </template> <!-- 动态插槽:如果配置了slotName,则使用对应插槽渲染 --> <template slot-scope="scope" > <!-- 优先使用自定义插槽 --> <slot v-if="column.slotName" :name="column.slotName" :row="scope.row" :index="scope.$index" :column="column" /> <!-- 无插槽时默认渲染字段值 --> <span v-else> {{ scope.row[column.prop] || "-" }} </span> </template> </el-table-column> </el-table> </template> <script> export default { name: "DynamicTable", props: { tableConfig: { type: Object, default: () => ({ selection: false, selectionWidth: "55", selectionFixed: true, talbeIndex: false, isExpand: false, expandSlotName: "", }), }, // 表格数据源(必传) tableData: { type: Array, required: true, default: () => [], }, // 表头配置数组(必传) columns: { type: Array, required: true, default: () => [], /* 配置项示例: [ { prop: 'id', label: 'ID', width: '80px' }, // 无插槽,默认渲染 { prop: 'name', label: '姓名', width: '120px' }, { prop: 'operation', label: '操作', width: '200px', slotName: 'operation' } // 自定义插槽 ] */ }, }, inheritAttrs: false, // 关闭属性继承,避免根元素继承非props属性 methods: { // 暴露给父组件的方法:获取选中行 getSelectedRows() { return this.$refs.dynamicTableRef?.getSelectionRows() || []; }, // 暴露给父组件的方法:清空选中行 clearSelection() { this.$refs.dynamicTableRef?.clearSelection(); }, // 暴露给父组件的方法:切换指定行的选中状态 toggleSelection(row) { this.$refs.dynamicTableRef?.toggleRowSelection(row); }, }, }; </script>
utils.js
// utils/func.js /** * 可配置防抖函数 * @param {Function} fn - 要防抖的函数 * @param {Number} delay - 延迟时间 * @param {Boolean} immediate - 是否首次立即执行,默认false(延迟执行) * @returns {Function} 包装后的防抖函数 */ export function debounce1(fn, delay = 500, immediate = false) { let timer = null; let isFirst = false; return function (...args) { // 清除之前的定时器 if (timer) clearTimeout(timer); // 首次立即执行的逻辑 if (immediate && !timer && !isFirst) { isFirst = true; fn.apply(this, args); } else if (isFirst) { timer = setTimeout(() => { fn.apply(this, args); timer = null; }, delay); } }; } // 防抖函数 export function debounce(fn, delay = 500) { let timer = null; // 返回包装后的函数,保留 this 指向和参数 return function (...args) { // 清除之前的定时器,重新计时 if (timer) clearTimeout(timer); timer = setTimeout(() => { fn.apply(this, args); // 执行原函数,传递参数和 this timer = null; // 执行后清空定时器 }, delay); }; } // 节流函数(时间戳版) export function throttle(fn, interval = 500) { let lastTime = 0; // 上一次执行的时间 return function (...args) { const now = Date.now(); // 距离上次执行超过间隔时间,才执行 if (now - lastTime >= interval) { fn.apply(this, args); lastTime = now; // 更新上次执行时间 } }; } export function getSpanArrCommon(data, mergeRules, mergeObj) { if (!data || !data.length || !mergeRules) { return; } // 清空旧的合并配置 Object.keys(mergeObj).forEach((key) => { mergeObj[key] = []; }); // 遍历每一列的合并规则 Object.entries(mergeRules).forEach(([colProp, groupFields]) => { let count = 0; // 合并起始行索引 mergeObj[colProp] = []; // 初始化当前列的合并数组 // 遍历每行数据计算合并数 data.forEach((item, index) => { if (index === 0) { // 第一行默认合并数为1 mergeObj[colProp].push(1); } else { // 生成分组标识(支持多字段组合) const getGroupKey = (row) => { return groupFields .map((field) => { const value = row[field]; return value === null || value === undefined ? "" : value; }) .join("|"); }; const currentKey = getGroupKey(item); const prevKey = getGroupKey(data[index - 1]); // 判断是否属于同一分组 if (currentKey === prevKey) { // 同一分组:起始行合并数+1,当前行标记为0 mergeObj[colProp][count] += 1; mergeObj[colProp].push(0); } else { // 不同分组:重置起始索引,当前行合并数为1 count = index; mergeObj[colProp].push(1); } } }); }); } const myName = () => { return "zs===="; }; export { myName }; export default { name: "zs", age: 22, };
6、el-table 数据扁平-处理成嵌套
看工具类
===================以下更乱😂===============================
多级表头--静态
<el-table :data="tableData" highlight-current-row @current-change="handleCurrentChange" style="width: 100%" > <el-table-column prop="date" label="日期" width="150"> </el-table-column> <el-table-column label="配送信息"> <el-table-column prop="name" label="姓名" width="120"> </el-table-column> <el-table-column label="地址"> <el-table-column prop="province" label="省份" width="120"> </el-table-column> <el-table-column prop="city" label="市区" width="120"> </el-table-column> <el-table-column prop="address" label="地址" width="300"> </el-table-column> <el-table-column prop="zip" label="邮编" width="120"> </el-table-column> </el-table-column> </el-table-column> </el-table>

2、多级表头--递归
<h1>多级表头--动态-组件递归</h1> 选择id:{{ tabSelIds }} - <el-button type="primary" @click="selcetAll">全选</el-button> <el-button type="primary" @click="cancelAll">取消全选</el-button> <el-button type="primary" @click="selectedIdsFn">默认勾选【1,3】</el-button> <el-table ref="myTable" :data="tableDataSyn" highlight-current-row @selection-change="handleSelectionChange" @row-click="currentSelect" :default-selection-keys="[1]" border style="width: 100%" > <el-table-column type="selection" width="55" :selectable="selectable"> </el-table-column> <template v-for="(item, index) in tableColumns"> <template v-if="item.children && item.children.length"> <table-column v-if="item.children && item.children.length" :key="index" :coloumn-header="item" ></table-column> </template> <el-table-column v-else-if="item.prop == 'operation'" :key="index" :label="item.label" :prop="item.prop" > <template slot-scope="scope"> <custom-button content="修改" @click.native.stop="handleTest(scope.row)" name="recording" /> </template> </el-table-column> <el-table-column v-else :key="index" :label="item.label" :prop="item.prop" ></el-table-column> <!-- --> <!-- <table-column v-if="item.children && item.children.length" :key="index" :coloumn-header="item" ></table-column> <el-table-column v-else :key="index" :label="item.label" :prop="item.prop" ></el-table-column> --> </template> </el-table>
组件部分。
TableHeader.vue
<template> <el-table-column :key="column.key || column.prop" :label="column.label" :prop="column.prop" :width="column.width" :min-width="column.minWidth || 80" :align="column.align || 'center'" :fixed="column.fixed" > <!-- 递归渲染子表头:有children则继续调用自身 --> <template v-if="column.children && column.children.length > 0"> <table-header v-for="(child, childIndex) in column.children" :key="childIndex" :column="child" /> </template> </el-table-column> </template> <script> export default { name: "TableHeader", // 必须命名,支持递归调用 props: { column: { type: Object, required: true, default: () => ({}) } } }; </script>
------- ------- ------- ------- ------- ------- ------- ------- ------- ------- ------- -------

完整代码,tableData 数据返回格式 扁平返回,表头可以有子集children
<template> <div class="large-table-container"> <!-- 开启虚拟滚动 + 懒加载,优化大量列渲染性能 --> <el-table :data="tableData" border style="width: 100%" :header-cell-style="{ background: '#f5f7fa' }" :virtual-scroll="true" :virtual-scroll-item-size="40" lazy :load="loadMoreData" :loading="tableLoading" > <!-- 引入递归表头组件,批量渲染所有表头 --> <table-header v-for="(col, idx) in dynamicHeaders" :key="idx" :column="col" /> </el-table> </div> </template> <script> import TableHeader from "./TableHeader.vue"; // 引入递归组件 export default { components: { TableHeader }, data() { return { tableLoading: false, // 模拟大量多级表头配置(可从接口获取) dynamicHeaders: [], // 表格数据 tableData: [], }; }, created() { // 初始化:模拟从接口获取大量表头配置 this.getLargeHeaderConfig(); // 初始化表格数据 this.getTableData(); }, methods: { // 模拟获取大量多级表头配置(支持任意层级) getLargeHeaderConfig() { // 实际项目中替换为真实接口请求 this.dynamicHeaders = [ { label: "基础信息", key: "base", children: [ { label: "个人信息", prop: "personal", children: [ { label: "姓名", prop: "name", width: 100 }, { label: "年龄", prop: "age", width: 80 }, { label: "性别", prop: "gender", width: 80 }, { label: "身份证号", prop: "idCard", width: 180 }, ], }, { label: "联系方式", prop: "contact", children: [ { label: "手机号", prop: "phone", width: 120 }, { label: "邮箱", prop: "email", width: 200 }, { label: "地址", prop: "address", children: [ { label: "省", prop: "province", width: 100, children: [ { label: "省1", prop: "province1", width: 200 }, { label: "省2", prop: "province2", width: 200 }, ], }, { label: "市", prop: "city", width: 100 }, { label: "区", prop: "area", width: 100 }, { label: "详细地址", prop: "detail", minWidth: 200 }, ], }, ], }, ], }, { label: "学业信息", key: "study", children: [ // 可无限嵌套,递归组件自动渲染 { label: "小学", key: "primary", children: [ { label: "学校名称", prop: "name", width: 150 }, { label: "入学时间", prop: "entryTime", width: 120 }, { label: "毕业时间", prop: "gradTime", width: 120 }, ], }, { label: "初中", key: "middle", children: [ { label: "学校名称", prop: "name", width: 150 }, { label: "入学时间", prop: "entryTime", width: 120 }, { label: "毕业时间", prop: "gradTime", width: 120 }, ], }, // 可继续添加高中、大学等层级... ], }, // 可继续添加更多大列(如家庭信息、奖惩信息等)... ]; }, // 模拟获取表格数据 getTableData() { this.tableData = [ { name: "张三", age: 20, gender: "男", idCard: "110101199901011234", phone: "13800138000", email: "zhangsan@example.com", city: "北京市", area: "朝阳区", detail: "XX小区1号楼1单元101", province1: "北京市1", province2: "北京市2", name: "XX小学", entryTime: "2010-09-01", gradTime: "2016-06-30", name: "XX中学", entryTime: "2016-09-01", gradTime: "2019-06-30", }, // 模拟多条数据... ]; }, // 懒加载加载更多数据(数据量大时用) loadMoreData(tree, treeNode, resolve) { this.tableLoading = true; // 模拟接口请求 setTimeout(() => { const newData = [ /* 新数据 */ ]; this.tableData = [...this.tableData, ...newData]; resolve(); this.tableLoading = false; }, 500); }, }, }; </script> <style scoped> .large-table-container { padding: 20px; overflow-x: auto; /* 列数过多时横向滚动 */ } /* 优化多级表头样式,避免层级过深导致文字挤压 */ ::v-deep .el-table th { font-weight: 600; white-space: nowrap; /* 表头文字不换行 */ } ::v-deep .el-table td { padding: 8px 0; } /* 虚拟滚动优化 */ ::v-deep .el-table__body-wrapper { overflow: auto; } </style>
浙公网安备 33010602011771号