uniapp 实现树形数据表格

image.png

代码实现

<template>
  <!-- 树形表格容器 -->
  <div class="tree-table-container">
    <!-- 表格主体 -->
    <table class="tree-table">
      <!-- 表头 -->
      <thead>
        <tr>
          <th>科目</th>
          <th>账户方向</th>
          <th>期初借方</th>
        </tr>
      </thead>
      <!-- 表格内容 -->
      <tbody>
        <!-- 循环渲染每一行数据 -->
        <tr v-for="item in flattenedData" :key="item.id" :class="{ 'has-children': item.children }">
          <!-- 科目列,带缩进 -->
          <td :style="{ paddingLeft: `${(item.level + 1) * 16}px` }">
            <!-- 展开/折叠图标 - 仅当有子项时显示 -->
            <!-- 判断是否包含已展开的子项,如果有,则显示-图标,否则显示+图标 -->
            <span v-if="item.children" @click="toggleExpand(item.id)" class="expand-icon">
              {{ expandedItems.includes(item.id) ? '−' : '+' }}
            </span>
            <!-- 科目名称 - 无子项时保持16px基础缩进 -->
            <span class="name-text">{{ item.name }}</span>
          </td>
          <!-- 账户方向列 -->
          <td>{{ item.date }}</td>
          <!-- 期初借方列 -->
          <td>{{ item.address }}</td>
        </tr>
      </tbody>
    </table>
  </div>
</template>

<script setup lang="ts">
// 导入Vue组合式API
import { ref, computed } from 'vue'

// 存储已展开的项目ID
const expandedItems = ref<number[]>([])

// 表格数据
const tableData = [
  {
    id: 1,
    date: '',
    name: 'XX公司',
    address: '4,106,236.00',
    children: [
      {
        id: 2,
        date: '借',
        name: '银行存款',
        address: '2.67',
        children: [
          {
            id: 21,
            date: '借',
            name: '工商银行',
            address: '2.67',
          },
        ],
      },
      {
        id: 3,
        date: '借',
        name: '其他应收款',
        address: '',
        children: [
          {
            id: 31,
            date: '',
            name: '郑州鱼头汽车有限公司',
            address: '',
            children: [
              {
                id: 311,
                date: '',
                name: '社保个人承担部分',
                address: '',
              },
              {
                id: 321,
                date: '',
                name: '社保中心',
                address: '',
              },
            ],
          },
          {
            id: 32,
            date: '测试',
            name: '第三级',
            address: '',
          },
        ],
      },
    ],
  },
  {
    id: 4,
    date: '',
    name: 'xx公司',
    address: '189',
  },
]

/**
 * 将树形数据扁平化
 * @param data 原始树形数据
 * @param level 当前层级
 * @param parentId 父级ID
 * @returns 扁平化后的数组
 - 通过flattenTree函数递归遍历树形数据
 - 为每个节点添加level(层级)和parentId(父节点ID)属性
 - 将嵌套结构转换为扁平数组,同时保留层级关系
 */
const flattenTree = (data: any[], level = 0, parentId?: number) => {
  return data.flatMap((item) => {
    const currentItem = { ...item, level, parentId }
    if (item.children) {
      console.log(item.children)
      return [currentItem, ...flattenTree(item.children, level + 1, item.id)]
    }
    return [currentItem]
  })
}

/**
 * 计算属性:获取当前显示的扁平化数据
 - 使用computed属性flattenedData动态计算要显示的行
 - 只显示level=0的根节点或父节点在expandedItems中的子节点
 - expandedItems数组记录用户已展开的节点ID
 */
const flattenedData = computed(() => {
  const allItems = flattenTree(tableData)
  console.log('allItems',allItems)
  //includes判断是否包含元素
  return allItems.filter((item) => item.level === 0 || expandedItems.value.includes(item.parentId))
})

/**
 * 切换展开/折叠状态
 * @param id 项目ID
 - 点击+/-图标切换子行显示/隐藏
 - 通过修改expandedItems数组控制展开状态
 - 数据变化会自动触发视图更新
 */
const toggleExpand = (id: number) => {
  const index = expandedItems.value.indexOf(id)
  if (index > -1) {
    expandedItems.value.splice(index, 1)
  } else {
    expandedItems.value.push(id)
  }
}
</script>

<style scoped lang="scss">
/* 表格容器样式 */
.tree-table-container {
  width: 100%;
  overflow-x: auto;
}

/* 表格样式 */
.tree-table {
  width: 100%;
  border-collapse: collapse;
  font-family: Arial, sans-serif;

  /* 单元格通用样式 */
  th, td {
    padding: 12px 12px;
    text-align: left;
    border: 1px solid #e0e0e0;
  }

  /* 表头样式 */
  th {
    background-color: #1976d2;
    color: white;
    font-weight: 500;
    position: sticky;
    top: 0;
  }

  /* 展开/折叠图标样式 */
  .expand-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 18px;
    height: 18px;
    background-color: #1976d2;
    color: white;
    border-radius: 50%;
    cursor: pointer;
    margin:0 8px;
    user-select: none;
    transition: all 0.2s ease;

    &:hover {
      background-color: #1565c0;
    }
  }

  /* 名称文本样式 */
  .name-text {
    display: inline-block;
    margin-left: 8px;
    vertical-align: middle;
    max-width: 100px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  /* 有子项的单元格样式 */
  .has-children > td {
    font-weight: bold;
  }

  /* 行悬停效果 */
  tr:hover {
    background-color: #f5f5f5;
  }
}
</style>

抽离成TreeTable组件

<template>
  <!-- 树形表格容器 -->
  <div class="tree-table-container">
    <!-- 表格主体 -->
    <table class="tree-table">
      <!-- 表头 -->
      <thead>
        <tr>
          <th v-for="(col, index) in columns" :key="index">{{ col.title }}</th>
        </tr>
      </thead>
      <!-- 表格内容 -->
      <tbody>
        <!-- 循环渲染每一行数据 -->
        <tr v-for="item in flattenedData" :key="item.id" :class="{ 'has-children': item.children }">
          <!-- 动态渲染列 -->
          <td v-for="(col, colIndex) in columns" :key="colIndex" 
              :style="colIndex === 0 ? { paddingLeft: `${(item.level + 1) * 16}px` } : {}">
            <!-- 第一列特殊处理 -->
            <template v-if="colIndex === 0">
              <!-- 展开/折叠图标 -->
              <span v-if="item.children" @click="toggleExpand(item.id)" class="expand-icon">
                {{ expandedItems.includes(item.id) ? '−' : '+' }}
              </span>
              <!-- 名称文本 -->
              <span class="name-text">{{ item[col.key] }}</span>
            </template>
            <template v-else>
              {{ item[col.key] }}
            </template>
          </td>
        </tr>
      </tbody>
    </table>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, defineProps, defineExpose } from 'vue'

interface Column {
  title: string
  key: string
}

interface TreeItem {
  id: number
  [key: string]: any
  children?: TreeItem[]
}

const props = defineProps<{
  data: TreeItem[]
  columns: Column[]
}>()

// 存储已展开的项目ID
const expandedItems = ref<number[]>([])

/**
 * 将树形数据扁平化
 */
const flattenTree = (data: any[], level = 0, parentId?: number) => {
  return data.flatMap((item) => {
    const currentItem = { ...item, level, parentId }
    if (item.children) {
      return [currentItem, ...flattenTree(item.children, level + 1, item.id)]
    }
    return [currentItem]
  })
}

/**
 * 计算属性:获取当前显示的扁平化数据
 */
const flattenedData = computed(() => {
  const allItems = flattenTree(props.data)
  return allItems.filter((item) => item.level === 0 || expandedItems.value.includes(item.parentId))
})

/**
 * 切换展开/折叠状态
 */
const toggleExpand = (id: number) => {
  const index = expandedItems.value.indexOf(id)
  if (index > -1) {
    expandedItems.value.splice(index, 1)
  } else {
    expandedItems.value.push(id)
  }
}
</script>

<style scoped lang="scss">
.tree-table-container {
  width: 100%;
  overflow-x: auto;
}

.tree-table {
  width: 100%;
  border-collapse: collapse;
  font-family: Arial, sans-serif;

  th, td {
    padding: 12px 12px;
    text-align: left;
    border: 1px solid #e0e0e0;
  }

  th {
    background-color: #1976d2;
    color: white;
    font-weight: 500;
    position: sticky;
    top: 0;
  }

  .expand-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 18px;
    height: 18px;
    background-color: #1976d2;
    color: white;
    border-radius: 50%;
    cursor: pointer;
    margin:0 8px;
    user-select: none;
    transition: all 0.2s ease;

    &:hover {
      background-color: #1565c0;
    }
  }

  .name-text {
    display: inline-block;
    margin-left: 8px;
    vertical-align: middle;
    max-width: 100px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  .has-children > td {
    font-weight: bold;
  }

  tr:hover {
    background-color: #f5f5f5;
  }
}
</style>

页面引入

<template>
  <TreeTable :data="tableData" :columns="columns" />
</template>

<script setup lang="ts">
import { default as TreeTable } from '@/components/TreeTable.vue'

// 表格列配置
const columns = [
  { title: '科目', key: 'name' },
  { title: '账户方向', key: 'date' },
  { title: '期初借方', key: 'address' },
]

// 表格数据
const tableData = [
  {
    id: 1,
    date: '',
    name: '河南XXXX公司',
    address: '4,106,236.00',
    children: [
      {
        id: 2,
        date: '借',
        name: '银行存款',
        address: '2.67',
        children: [
          {
            id: 21,
            date: '借',
            name: '工商银行',
            address: '2.67',
          },
        ],
      },
      {
        id: 3,
        date: '借',
        name: '其他应收款',
        address: '',
        children: [
          {
            id: 31,
            date: '',
            name: '郑州鱼头汽车有限公司',
            address: '',
            children: [
              {
                id: 311,
                date: '',
                name: '社保个人承担部分',
                address: '',
              },
              {
                id: 321,
                date: '',
                name: '社保中心',
                address: '',
              },
            ],
          },
          {
            id: 32,
            date: '测试',
            name: '第三级',
            address: '',
          },
        ],
      },
    ],
  },
  {
    id: 4,
    date: '',
    name: 'xx公司',
    address: '189',
  },
]
</script>
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容