基于 elements3 包装的 可展开 table 组件

发布于:2025-07-20 ⋅ 阅读:(11) ⋅ 点赞:(0)

基于 elements3 包装的 可展开 table 组件

1.手风琴效果,一次只能展开一个

2.动态加载 展开内容 (根据接口)

效果:

<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>

使用:
 

    <CustomExpandTable
      ref="expandTable"
      :columns="tableData.head"
      :tableData="tableData.data"
      :fetchChildData="getSubTable"
      rowKey="stattime"></CustomExpandTable>


网站公告

今日签到

点亮在社区的每一天
去签到