基于 elements3 包装的 可展开 table 组件
1.手风琴效果,一次只能展开一个
2.动态加载 展开内容 (根据接口)
效果:

TypeScript
<template>
<el-table
ref="tableRef"
:data="tableData"
v-loading="loading"
v-if="columns.length > 0"
style="width: 100%"
@expand-change="handleExpand"
class="custom-expend-table"
header-row-class-name="custom-table-header"
row-class-name="custom-table-row"
@row-click="handleClick"
>
<!-- 折叠列 -->
<el-table-column type="expand" v-if="hasExpand" width="20">
<template #default="{ row }">
<div class="expand-content-wrapper">
<el-table
v-if="childDataMap[row[rowKey]]?.length"
:data="childDataMap[row[rowKey]]"
size="small"
header-row-class-name="child-table-header"
row-class-name="child-table-row"
>
<el-table-column
v-for="col in childColumns"
:key="col.column"
:label="col.showName"
:prop="col.column"
:min-width="col.width || col.column === 'month' ? 50 : 80"
align="center"
>
<template #default="{ row }">
{{ row[col.column] || '-' }}
</template>
</el-table-column>
</el-table>
<div v-else class="text-gray-500 text-center py-4 empty-box">
{{ expandLoadingMap[row[rowKey]] ? '加载中...' : '暂无数据' }}
</div>
</div>
</template>
</el-table-column>
<!-- 主表列 -->
<el-table-column
v-for="col in columns"
:key="col.column"
:prop="col.column"
:label="col.showName"
:min-width="col.width || col.column === 'year' ? 50 : 80"
align="center"
>
<template #default="{ row }">
{{ row[col.column] || '-' }}
</template>
</el-table-column>
<template #empty>
<EmptyBox desc="无数据" v-if="height" :height="'auto'" />
<EmptyBox desc="无数据" v-else />
</template>
</el-table>
</template>
<script setup lang="ts">
import { ref , PropType,computed } from 'vue'
import { ElMessage } from 'element-plus'
import {sortByDate} from "@/utils/formate/sort_formate";
import { isEqual } from 'lodash-es';
const props = defineProps({
height: {
type: [String, Boolean],
default: '180'
},
tableData: {
type: Array as PropType<any[]>,
required: true,
},
columns: {
type: Array as PropType<any[]>, // 使用定义好的 Column 类型
required: true,
}, // 主表字段
rowKey: {
type: String,
default: 'id',
},
fetchChildData: {
type: Function || Function as PropType<(row: any) => Promise<any>> ,
required: false,
default: undefined
},
loading: {
type: Boolean,
default: false,
},
isThrottle: {
type: Boolean,
default: false,
}
})
const hasExpand = computed(() => typeof props.fetchChildData === 'function')
// 保存每行子表数据
const childDataMap = ref<Record<string, any[]>>({})
const childColumns = ref<any[]>([])
const expandLoadingMap = ref<Record<string, boolean>>({})
// 展开行触发:异步加载子表
const handleExpand = async (row: any, expanded: any[]) => {
const rowKey: any = row[props.rowKey]
// 当前展开的 rowKey(row 点击后立即触发)
const isExpanding: any = expanded.some((t: any) => rowKey == t[props.rowKey])
// 收起时清空记录
if (!isExpanding) {
if (currentExpandedRowKey.value === rowKey) {
currentExpandedRowKey.value = null
}
return
}
// 若当前点击的不是同一行,先收起上一个
if (currentExpandedRowKey.value && currentExpandedRowKey.value !== rowKey) {
const prevRow = props.tableData?.find(
(item: any) => item[props.rowKey] === currentExpandedRowKey.value
)
if (prevRow && tableRef.value) {
tableRef.value.toggleRowExpansion(prevRow, false)
}
}
// 设置当前展开行为当前行
currentExpandedRowKey.value = rowKey
// 判断是否需要加载数据
if (props.isThrottle && childDataMap.value[rowKey]) return
expandLoadingMap.value[rowKey] = true
try {
if (!props.fetchChildData) return
const { head, data } = await props.fetchChildData(row)
sortByDate(data, 'month', 'desc')
childDataMap.value[rowKey] = data || []
childColumns.value = head || []
} catch (err) {
ElMessage.error('子表加载失败')
childDataMap.value[rowKey] = []
} finally {
expandLoadingMap.value[rowKey] = false
}
}
const tableRef = ref()
const currentExpandedRowKey = ref<string | null>(null)
const currentExpandedRow = ref<any>(null)
const handleClick = (row: any) => {
if (!hasExpand.value) return
const rowKey = row[props.rowKey]
// 从 tableData 中找到真正要展开的那一行(必要)
const matchedRow: any = props.tableData?.find((item: any) => item[props.rowKey] === rowKey)
if (!matchedRow) {
console.warn('找不到匹配的行数据:', rowKey)
return
}
// 👉 判断是否是"当前已展开的那一行"
const isSameRow = (
currentExpandedRowKey.value === rowKey &&
isEqual(currentExpandedRow.value, matchedRow)
)
// 如果点击的是当前已展开行 → 收起
if ( isSameRow ) {
tableRef.value.toggleRowExpansion(matchedRow, false)
currentExpandedRowKey.value = null
currentExpandedRow.value = null
return
}
// 收起之前已展开的
if (currentExpandedRowKey.value !== null) {
const prevRow = props.tableData.find((item: any) => item[props.rowKey] === currentExpandedRowKey.value)
if (prevRow) {
tableRef.value.toggleRowExpansion(prevRow, false)
}
}
// 展开新行
tableRef.value.toggleRowExpansion(matchedRow, true)
currentExpandedRowKey.value = rowKey
currentExpandedRow.value = matchedRow
}
defineExpose({
handleClick,
});
</script>
使用:
TypeScript
<CustomExpandTable
ref="expandTable"
:columns="tableData.head"
:tableData="tableData.data"
:fetchChildData="getSubTable"
rowKey="stattime"></CustomExpandTable>