HarmonyOS 鸿蒙Next开发实战:实现树形单位结构展示

HarmonyOS 鸿蒙Next开发实战:实现树形单位结构展示 在鸿蒙 ArkTS 开发中,需要实现集团组织架构的树形结构展示,过程中遇到以下问题:

  1. 递归渲染子节点时出现类型错误(Property 'node' has no initializer);
  2. ForEach泛型写法导致语法报错(Expected 0 type arguments, but got 1);
  3. 布局属性使用错误(marginTop(16)提示无此函数);
  4. 树形结构 UI 展示效果差,层级不清晰、交互体验不佳。
8 回复

6

更多关于HarmonyOS 鸿蒙Next开发实战:实现树形单位结构展示的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


加油,

很好的文章,想问下能否实现默认展开所有子节点

可以的,实现默认展开所有节点的核心是递归遍历树形数据,在组件初始化时将所有节点的isExpanded属性设置为true,

HarmonyOS的社区里有很多技术大牛分享经验,学到了很多有用的知识。

一、原理解析

树形结构展示的核心是递归组件渲染:通过自定义组件递归调用自身,根据节点层级动态缩进,结合状态管理控制节点展开 / 折叠。ArkTS 中需注意:

组件属性必须初始化,避免类型检查报错;

ForEach不支持泛型语法,需通过显式类型标注保证类型安全;

布局属性(如margin/padding)需使用对象格式,而非函数调用。

二、解决步骤

  1. 定义树形数据模型

首先定义统一的节点数据结构,包含必要属性:

// TreeNodeModel.ets
export interface TreeNode {
  id: string;                 // 节点唯一标识
  name: string;               // 单位名称
  desc?: string;              // 单位描述
  children?: TreeNode[];      // 子节点
  isExpanded?: boolean;       // 是否展开
  isSelected?: boolean;       // 是否选中
}
  1. 核心组件实现(注意样式避坑)

修复属性初始化问题:使用{} as TreeNode初始化节点;

移除ForEach泛型语法,改用显式类型标注;

统一布局属性格式:margin({ top: 16 })替代marginTop(16)。

  1. 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 }); // 子节点缩进
      }
    };
  }
}

四、效果展示

cke_51356.png

鸿蒙Next中实现树形单位结构展示,可通过TreeContainer组件实现。该组件专为层级数据设计,支持展开/折叠节点交互。数据源需使用TreeDataSource对象,其中每个节点需包含唯一标识符、父节点ID及显示内容。通过TreeContaineronExpandonCollapse回调可处理节点状态变化。若需自定义节点样式,可重写TreeContainerbuildItem方法。该方案完全基于ArkTS开发,符合鸿蒙应用开发规范。

针对树形结构展示的问题,建议如下解决方案:

  1. 类型错误问题:在ArkTS中需明确定义节点类型,建议使用接口定义节点结构:
interface TreeNode {
  id: string
  name: string
  children?: TreeNode[]
}

并在组件类中初始化节点属性,避免未定义错误。

  1. ForEach泛型问题:鸿蒙的ForEach语法为:
ForEach(
  this.treeData,
  (item: TreeNode) => {
    // 渲染逻辑
  },
  (item: TreeNode) => item.id
)

不需要显式声明泛型参数。

  1. 布局属性问题:鸿蒙布局系统使用链式调用,正确写法:
Row()
  .margin({ top: 16 })
  .padding(10)
  1. 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
          )
        }
      }
    }
  }
}

注意在父组件中管理展开状态,通过@Link@Prop实现状态同步。

回到顶部