HarmonyOS 鸿蒙Next开发实战:实现树形单位结构展示
HarmonyOS 鸿蒙Next开发实战:实现树形单位结构展示 在鸿蒙 ArkTS 开发中,需要实现集团组织架构的树形结构展示,过程中遇到以下问题:
- 递归渲染子节点时出现类型错误(
Property 'node' has no initializer); ForEach泛型写法导致语法报错(Expected 0 type arguments, but got 1);- 布局属性使用错误(
marginTop(16)提示无此函数); - 树形结构 UI 展示效果差,层级不清晰、交互体验不佳。
加油,
很好的文章,想问下能否实现默认展开所有子节点
可以的,实现默认展开所有节点的核心是递归遍历树形数据,在组件初始化时将所有节点的isExpanded属性设置为true,
HarmonyOS的社区里有很多技术大牛分享经验,学到了很多有用的知识。
一、原理解析
树形结构展示的核心是递归组件渲染:通过自定义组件递归调用自身,根据节点层级动态缩进,结合状态管理控制节点展开 / 折叠。ArkTS 中需注意:
组件属性必须初始化,避免类型检查报错;
ForEach不支持泛型语法,需通过显式类型标注保证类型安全;
布局属性(如margin/padding)需使用对象格式,而非函数调用。
二、解决步骤
- 定义树形数据模型
首先定义统一的节点数据结构,包含必要属性:
// TreeNodeModel.ets
export interface TreeNode {
id: string; // 节点唯一标识
name: string; // 单位名称
desc?: string; // 单位描述
children?: TreeNode[]; // 子节点
isExpanded?: boolean; // 是否展开
isSelected?: boolean; // 是否选中
}
- 核心组件实现(注意样式避坑)
修复属性初始化问题:使用{} as TreeNode初始化节点;
移除ForEach泛型语法,改用显式类型标注;
统一布局属性格式:margin({ top: 16 })替代marginTop(16)。
- UI 美化优化
采用卡片式设计,增加圆角、阴影提升层次感;
通过层级缩进 + 展开 / 折叠标识区分节点层级;
添加悬浮 / 选中状态交互,提升用户体验。
三、完整示例代码
import { TreeNode } from './model/TreeNodeModel';
@Entry
@Component
struct TreeUnitPage {
// 模拟单位树形数据(支持多级嵌套)
@State treeData: TreeNode[] = [
{
id: "root",
name: "XX集团总公司",
desc: "集团总部",
isExpanded: true,
isSelected: false,
children: [
{
id: "branch1",
name: "研发中心",
desc: "技术研发部门",
isExpanded: true,
children: [
{ id: "dept1-1", name: "前端开发部", desc: "负责鸿蒙/前端开发" },
{ id: "dept1-2", name: "后端开发部", desc: "负责服务端开发" },
{
id: "dept1-3",
name: "测试部",
children: [
{ id: "team1-3-1", name: "功能测试组" },
{ id: "team1-3-2", name: "性能测试组" }
]
}
]
},
{
id: "branch2",
name: "业务中心",
children: [
{ id: "dept2-1", name: "市场部" },
{ id: "dept2-2", name: "销售部" }
]
},
{ id: "branch3", name: "行政中心", desc: "行政后勤管理" }
]
}
];
build() {
Column() {
// 页面头部
Stack() {
Row()
.width('100%')
.height(60)
.backgroundColor('#2f54eb')
.borderRadius({ bottomLeft: 12, bottomRight: 12 });
Text("集团组织架构")
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
.margin({top:16});
}
// 树形结构容器(可滚动)
Scroll() {
Column({ space: 8 }) { // 增加节点间距
// 渲染根节点
ForEach(this.treeData, (node: TreeNode) => {
TreeItemComponent({ node: node, level: 0 });
}, (node: TreeNode) => node.id);
}
.padding(12)
}
.backgroundColor('#f8f9fa')
.flexGrow(1)
.margin({ top: 12, left: 12, right: 12, bottom: 12 })
.borderRadius(12)
.shadow({ radius: 4, color: '#00000008', offsetX: 0, offsetY: 2 });
}
.width('100%')
.height('100%')
.backgroundColor('#e8eaf6');
}
}
/**
* 树形节点子组件(递归渲染)
*/
@Component
struct TreeItemComponent {
@State node: TreeNode = {} as TreeNode;
@Prop level: number;
@State isHover: boolean = false; // 悬浮状态
// 切换节点展开/折叠状态
private toggleExpand() {
if (this.node.children?.length) {
this.node.isExpanded = !this.node.isExpanded;
}
}
// 切换节点选中状态
private toggleSelect() {
this.node.isSelected = !this.node.isSelected;
}
build() {
Column() {
// 节点内容行
Row({ space: 10 }) {
// 层级缩进占位
Blank().width(this.level * 24); // 调整缩进宽度
// 展开/折叠按钮(美化)
if (this.node.children?.length) {
Stack() {
Text(this.node.isExpanded ? "▼" : "▶")
.fontSize(14)
.fontColor('#4096ff');
}
.width(24)
.height(24)
.backgroundColor(this.isHover ? '#e6f7ff' : '#f0f7ff')
.borderRadius(6)
.onClick(() => this.toggleExpand())
.onHover((isHover: boolean) => {
this.isHover = isHover;
});
} else {
Blank().width(24); // 占位对齐
}
// 节点主体内容
Column({ space: 4 }) {
Text(this.node.name)
.fontSize(16)
.fontWeight(this.node.isSelected ? FontWeight.Bold : FontWeight.Medium)
.fontColor(this.node.isSelected ? '#2f54eb' : '#1d2129');
if (this.node.desc) {
Text(this.node.desc)
.fontSize(12)
.fontColor('#666')
.opacity(0.8);
}
}
.flexGrow(1)
.alignItems(HorizontalAlign.Start);
// 选中状态标记(美化)
if (this.node.isSelected) {
Stack() {
Text("✓")
.fontSize(16)
.fontColor(Color.White);
}
.width(24)
.height(24)
.backgroundColor('#2f54eb')
.borderRadius(12);
} else {
Blank().width(24);
}
}
.padding({ left: 16, right: 16, top: 14, bottom: 14 })
.backgroundColor(this.node.isSelected ? '#f0f7ff' : Color.White)
.borderRadius(10)
.border({ width: this.node.isSelected ? 1 : 0, color: '#2f54eb' })
.shadow({
radius: this.node.isSelected ? 6 : 2,
color: this.node.isSelected ? '#2f54eb10' : '#00000005',
offsetX: 0,
offsetY: this.node.isSelected ? 3 : 1
})
.onClick(() => this.toggleSelect())
.onHover((isHover: boolean) => {
this.isHover = isHover;
})
.animation({ duration: 100 }); // 过渡动画
// 递归渲染子节点(仅展开时显示)
if (this.node.isExpanded && this.node.children?.length) {
Column({ space: 8 }) { // 子节点间距
ForEach(this.node.children, (childNode: TreeNode) => {
TreeItemComponent({ node: childNode, level: this.level + 1 });
}, (child: TreeNode) => child.id);
}
.margin({ top: 4, left: 12 }); // 子节点缩进
}
};
}
}
四、效果展示

鸿蒙Next中实现树形单位结构展示,可通过TreeContainer组件实现。该组件专为层级数据设计,支持展开/折叠节点交互。数据源需使用TreeDataSource对象,其中每个节点需包含唯一标识符、父节点ID及显示内容。通过TreeContainer的onExpand和onCollapse回调可处理节点状态变化。若需自定义节点样式,可重写TreeContainer的buildItem方法。该方案完全基于ArkTS开发,符合鸿蒙应用开发规范。
针对树形结构展示的问题,建议如下解决方案:
- 类型错误问题:在ArkTS中需明确定义节点类型,建议使用接口定义节点结构:
interface TreeNode {
id: string
name: string
children?: TreeNode[]
}
并在组件类中初始化节点属性,避免未定义错误。
- ForEach泛型问题:鸿蒙的ForEach语法为:
ForEach(
this.treeData,
(item: TreeNode) => {
// 渲染逻辑
},
(item: TreeNode) => item.id
)
不需要显式声明泛型参数。
- 布局属性问题:鸿蒙布局系统使用链式调用,正确写法:
Row()
.margin({ top: 16 })
.padding(10)
- UI优化建议:
- 使用缩进表示层级关系
- 添加展开/收起图标(使用if/else控制子节点显示)
- 为节点添加点击事件处理展开状态
- 建议使用List容器提升滚动性能
示例核心代码结构:
@Component
struct TreeNodeComponent {
[@Link](/user/Link) node: TreeNode
build() {
Column() {
Row() {
Text(this.node.name)
if (this.node.children) {
Image($r('app.media.arrow')) // 展开图标
}
}
.onClick(() => {
// 切换展开状态
})
if (this.node.expanded && this.node.children) {
Column() {
ForEach(
this.node.children,
(child: TreeNode) => {
TreeNodeComponent({ node: child })
},
(child: TreeNode) => child.id
)
}
}
}
}
}

