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>