在这里插入图片描述

在这里插入图片描述

1. 引言:从数据管理看布局设计

在移动应用开发中,数据管理类应用是最常见的类型之一——通讯录、订单列表、课程表、员工花名册、学生成绩册……它们的核心需求高度相似:展示数据列表、提供筛选搜索、查看条目详情、展示统计概览。这类应用的设计难点从来不在于某个炫酷的交互特效,而在于如何用最简洁的布局结构承载最多的信息量。

想象一下你面前有一个班级的花名册:你需要一眼看到全班有多少人、男女比例是多少、平均成绩如何;你需要能够按班级筛选、查看某个学生的详细信息;你还需要快速操作——添加新人、查看统计报表。所有这些需求都要在一个手机屏幕上完成,而且不能让人觉得拥挤或混乱。这就是布局设计要解决的核心问题。

这类应用的界面布局通常遵循一个经过多年验证的固定模式:

顶部:标题 + 统计概览 + 筛选条件
中部:可滚动的数据列表(核心内容区)
底部:操作按钮 + 辅助信息

在鸿蒙 ArkUI 框架中,这种"三段式"布局的最佳实现方案就是 Column + layoutWeight 弹性布局。固定区用 height() 锁定高度,弹性区用 layoutWeight() 按比例分配剩余空间。这样无论屏幕尺寸如何变化,顶部和底部始终可见,中间内容区自动适配填满所有可用空间。

本文将以一个完整的学生管理应用为载体,系统讲解如何利用 Column + layoutWeight 构建响应式的数据管理界面。这个应用包含了 24 名学生的完整数据,横跨三个班级,包含姓名、性别、年龄、专业、分数、电话等八个字段。它支持班级筛选、展开详情、实时统计和操作按钮,所有交互都基于 @State 响应式状态管理驱动。无论是布局结构还是交互模式,都可以作为通用数据管理应用开发的参考模板。


2. 项目全景:学生管理应用的架构设计

2.1 项目结构

entry/src/main/ets/pages/StudentApp.ets   ← 唯一页面(511 行)
entry/src/main/resources/base/profile/
  └── main_pages.json                      ← 页面路由
entry/src/main/ets/entryability/
  └── EntryAbility.ets                      ← Ability 入口

2.2 SDK 版本

build-profile.json5 中确认了 SDK 版本:

{
  "products": [{
    "name": "default",
    "targetSdkVersion": "6.1.1(24)",
    "compatibleSdkVersion": "6.1.1(24)",
    "runtimeOS": "HarmonyOS"
  }]
}

2.3 页面布局架构

整个页面由最外层 Column 撑满全屏,内部包含 5 个区段:

Column()                             .width('100%').height('100%')
├── 固定顶部  .height(68)             ← 标题 + 班级筛选标签
├── 弹性区 A  .layoutWeight(1.0)      ← 50% 学生列表(可滚动)
├── 弹性区 B  .layoutWeight(0.4)      ← 20% 统计概览
├── 弹性区 C  .layoutWeight(0.6)      ← 30% 操作按钮
└── 固定底部  内容撑高                 ← 布局要点说明

弹性分配公式(以 800vp 屏幕为例):

弹性空间 = 800 - 68(顶) - 说明区(≈140) ≈ 592vp

学生列表 = 592 × 1.0/(1.0+0.4+0.6) = 592 × 50% ≈ 296vp
统计概览 = 592 × 0.4/2.0 = 592 × 20% ≈ 118vp
操作按钮 = 592 × 0.6/2.0 = 592 × 30% ≈ 178vp

2.4 交互功能矩阵

功能 技术实现 用户操作
班级筛选 @State filterClass + getter filteredList 点击顶部班级标签
展开详情 @State selectedId + 条件渲染 if 点击学生卡片
实时统计 getter 计算 avgScore/maxScore/passRate 自动更新
操作按钮 Button + onClick + Toast 点击按钮
分数着色 scoreColor() 条件返回 自动应用

3. Column + layoutWeight 弹性布局的前世今生

3.1 layoutWeight 的本质

ArkUI 中的 layoutWeight 是一个子组件属性,而不是一个独立的组件。这与 Flutter 等框架存在根本区别——Flutter 使用 <Expanded> 包裹组件,而 ArkUI 直接在子组件上调用方法。这种设计让代码更加扁平,减少了不必要的嵌套层级。

它的作用是告诉父容器:“我愿意参与剩余空间的分配,这是我的权重。”

Column() {
  Row() { /* 标题 */ }.height(68)           // 固定高度 68vp
  Column() { /* 列表 */ }.layoutWeight(1.0) // 吃掉 50% 的剩余空间
  Column() { /* 统计 */ }.layoutWeight(0.4) // 吃掉 20%
  Column() { /* 按钮 */ }.layoutWeight(0.6) // 吃掉 30%
  Column() { /* 说明 */ }                    // 由内容撑高,不参与弹性分配
}

这里有一个细节值得注意:最后一个 Column 没有设置 layoutWeight,也没有设置 height。它的高度由其内部的内容撑开。这意味着它不参与弹性空间分配,但它的高度会计入"固定高度"的总和,从 Column 的可用高度中减去。这说明一个 Column 的子组件可以混合使用固定高度、弹性权重和内容撑高三种模式——ArkUI 会先计算固定高度,再扣除内容撑高的部分,最后把剩余空间按权重分配给弹性区。

3.2 weight 不是 flex

很多从 CSS 转过来的开发者会认为 layoutWeight 等同于 CSS Flexbox 中的 flex 属性。它们确实类似,但有一个关键区别:CSS 的 flex 属性是一个复合属性(包含了 flex-growflex-shrinkflex-basis),它同时控制了子组件的拉伸比例、收缩比例和初始尺寸。而 ArkUI 的 layoutWeight 只做一件事——按比例分配父容器的剩余空间。它不控制子组件的收缩行为,也不设置初始尺寸。

/* CSS flex:一个属性控制三种行为 */
.item {
  flex: 1;  /* flex-grow: 1, flex-shrink: 1, flex-basis: 0 */
}
// ArkUI layoutWeight:只做一件事
Column() { }.layoutWeight(1.0)  // 只控制剩余空间分配比例

这种简洁设计的背后是 ArkUI 的布局哲学:明确区分固定尺寸和弹性尺寸。固定尺寸用 height(),弹性尺寸用 layoutWeight(),没有模棱两可的中间地带。开发者不需要理解 flex-grow 和 flex-shrink 的区别,只需要知道"这个组件要固定高度还是弹性比例"。

3.3 为什么必须设置 height(‘100%’)

这是所有 ArkTS 初学者最容易踩的坑,也是在论坛和社区中被问得最多的问题。Column 如果不设置 height('100%'),它的高度将由子组件撑开——也就是说,Column 的高度刚好等于所有子组件的高度之和。在这种情况下,Column 没有"剩余空间"可以分配:它的可用空间已经被子组件占满了。

layoutWeight 试图计算"我应该占多少空间"时,它的计算公式是:

实际高度 = (父容器高度 - 固定高度之和) × (自身权重 / 总权重)

如果父容器没有固定高度(也没有 height(‘100%’)),那么父容器高度等于子组件高度之和,于是 父容器高度 - 固定高度之和 ≈ 0,乘以任何权重都等于零。这就是为什么 layoutWeight 设置了却不起作用。

因此,每次使用 layoutWeight 时,都要确认最外层 Column 设置了 .height('100%')。这是一个一劳永逸的设置——写一次,永远有效。


4. 三段式弹性布局在数据管理中的应用

4.1 代码结构

build() {
  Column() {
    // ── 固定顶部 ──
    Column() { /* 标题 + 筛选标签 */ }
      .height(68)

    // ── 弹性区 A:学生列表 ──
    Column() {
      Scroll() {
        Column({ space: 0 }) {
          ForEach(...) { /* 学生卡片 */ }
        }
      }
      .layoutWeight(1)
    }
    .layoutWeight(1.0)

    // ── 弹性区 B:统计概览 ──
    Column() { /* 平均分 | 最高分 | 及格率 */ }
      .layoutWeight(0.4)

    // ── 弹性区 C:操作按钮 ──
    Column() { /* 添加 | 统计 | 刷新 */ }
      .layoutWeight(0.6)

    // ── 固定底部 ──
    Column() { /* 布局要点 */ }
  }
  .width('100%')
  .height('100%')
}

4.2 弹性分配的可视化效果

当屏幕高度变化时:

  • 固定顶部(68vp):始终保持 68vp,不变化
  • 学生列表(50%):屏幕越高,可见的学生条目越多;屏幕越矮,条目越少但可滚动
  • 统计概览(20%):始终保持足够显示三个数字和一排文字
  • 操作按钮(30%):始终保持足够容纳三个按钮
  • 说明面板:由内容撑高,内容固定,高度固定

这种分层设计确保了关键信息始终可见,即使在较小的屏幕上也不会丢失功能。


5. 数据模型设计:Student 接口与样例数据

5.1 接口定义

interface Student {
  id: number;
  name: string;
  gender: string;     // 男 / 女
  age: number;
  className: string;  // 班级
  score: number;      // 总分
  major: string;      // 专业
  phone: string;      // 电话号码
}

interface ClassInfo {
  name: string;
  studentCount: number;
}

Student 接口包含了学生管理最核心的 8 个字段。id 作为唯一标识用于列表渲染的 key 和展开详情的判断。

5.2 样例数据

24 名学生分布在 3 个班级,每个班级 8 人:

const STUDENTS: Student[] = [
  { id: 101, name: '张明', gender: '男', age: 18, className: '高一(1)班',
    score: 92, major: '理科', phone: '138****1234' },
  { id: 102, name: '李华', gender: '女', age: 17, className: '高一(1)班',
    score: 88, major: '理科', phone: '138****5678' },
  // ... 共 24 条
];

const CLASSES: ClassInfo[] = [
  { name: '高一(1)班', studentCount: 8 },
  { name: '高一(2)班', studentCount: 8 },
  { name: '高一(3)班', studentCount: 8 },
];

数据的分布覆盖了不同分数段(61~98 分)、性别比例(男女各约一半)、专业分布(文理科各占一定比例),让筛选和统计功能有实际意义。


6. 响应式状态管理:@State 驱动筛选与展开

6.1 两个核心状态变量

@State private selectedId: number = -1;   // 当前展开的学生 id
@State private filterClass: string = '';    // 筛选的班级名

selectedId 控制学生卡片的展开/收起。-1 表示没有展开任何卡片,其他值表示展开了对应 id 的学生。

filterClass 控制列表筛选。空字符串表示显示全部学生,非空字符串表示只显示该班级的学生。

6.2 getter 驱动的筛选逻辑

private get filteredList(): Student[] {
  if (this.filterClass.length <= 0) {
    return STUDENTS;
  }
  let result: Student[] = [];
  for (let i: number = 0; i < STUDENTS.length; i++) {
    if (STUDENTS[i].className === this.filterClass) {
      result[result.length] = STUDENTS[i];
    }
  }
  return result;
}

getter 每次被访问时重新计算,直接依赖于 filterClass 的值。当 filterClass 变化时,ArkUI 框架自动重新渲染所有依赖 filteredList 的 UI 部分。

6.3 数据流链路

点击班级标签
  → setFilter(cls) 修改 @State filterClass
    → filteredList 重新计算
      → ForEach 重新渲染列表
        → totalCount / avgScore 等统计重新计算
          → UI 更新

这是一个完整的单向数据流:用户操作 → 状态变更 → 数据重算 → UI 更新。不需要手动操作 DOM,不需要通知机制,ArkUI 的响应式系统自动完成所有中间步骤。


7. 列表渲染:Scroll + Column + Divider 实现学生卡片列表

7.1 核心结构

Scroll() {
  Column({ space: 0 }) {
    ForEach(this.filteredList, (stu: Student, idx: number) => {
      if (idx > 0) {
        Divider().height(1).color('#E0E0E0').margin({ top: 2, bottom: 2 })
      }
      // 学生卡片...
    }, (stu: Student) => stu.id.toString())
  }
}
.layoutWeight(1)

Scroll 包裹 Column 实现可滚动列表,Column 的 space 设为 0 表示间距由 Divider 的 margin 控制。

7.2 学生卡片概要行

Row() {
  // 姓名
  Text(stu.name).fontSize(15).fontWeight(FontWeight.Bold).fontColor('#1A237E')
  // 性别图标
  Text(stu.gender === '男' ? ' ♂' : ' ♀')
    .fontColor(stu.gender === '男' ? '#1565C0' : '#E91E63')
  // 班级标签
  Text(stu.className).fontSize(11)
    .backgroundColor('#F5F5F5').borderRadius(4)
  Blank()
  // 分数 + 等级
  Text('' + stu.score).fontColor(this.scoreColor(stu.score))
  Text(this.scoreLabel(stu.score))
  // 展开箭头
  Text(this.selectedId === stu.id ? '▲' : '▼')
}
.onClick(() => { this.toggleDetail(stu.id); })

每行包含姓名、性别图标、班级标签、分数、等级标签、展开箭头,外加点击展开详情。信息密度适中,一目了然。

7.3 Divider 的分隔作用

条目之间的 Divider 使用极浅灰色(#E0E0E0)和小间距(margin: 2),在视觉上提供轻微的分隔,既区分了条目边界,又不会喧宾夺主。


8. 条件展开:点击卡片查看详情

8.1 展开逻辑

private toggleDetail(id: number): void {
  if (this.selectedId === id) {
    this.selectedId = -1;   // 点击已展开的卡片 → 收起
  } else {
    this.selectedId = id;   // 点击其他卡片 → 展开
  }
}

这是一个典型的手风琴(Accordion)交互模式——点击展开,再次点击或点击其他项时切换展开状态。

8.2 条件渲染详情区域

if (this.selectedId === stu.id) {
  Column() {
    Divider().height(1).color('#E8E8E8')

    Row() {
      Text('年龄:').fontSize(12).fontColor('#9E9E9E').width(48)
      Text('' + stu.age + ' 岁')
      Blank()
      Text('专业:').fontSize(12).fontColor('#9E9E9E').width(48)
      Text(stu.major)
    }

    Row() {
      Text('电话:').fontSize(12).fontColor('#9E9E9E').width(48)
      Text(stu.phone)
      Blank()
      Text('学号:').fontSize(12).fontColor('#9E9E9E').width(48)
      Text('' + stu.id)
    }
  }
}

详情区域使用 if (this.selectedId === stu.id) 条件渲染。当条件不满足时,ArkUI 不会创建这些组件,完全避免了隐藏/显示切换的开销。

8.3 两列布局的细节

详情中的信息使用 Row + Text(标签) + Text(值) + Blank() + Text(标签) + Text(值) 的布局,将四个字段排列为两行两列:

年龄:18 岁              专业:理科
电话:138****1234        学号:101

每个标签使用固定宽度 48vp 右对齐,值使用自适应宽度,Blank() 占据中间剩余空间将两列推向两侧。


9. 实时统计:getter 计算平均分、最高分和及格率

9.1 统计 getter

/** 平均分 */
private get avgScore(): number {
  if (this.totalCount <= 0) return 0;
  let sum: number = 0;
  for (let i: number = 0; i < this.filteredList.length; i++) {
    sum = sum + this.filteredList[i].score;
  }
  return Math.round(sum / this.totalCount);
}

/** 最高分 */
private get maxScore(): number {
  let m: number = 0;
  for (let i: number = 0; i < this.filteredList.length; i++) {
    if (this.filteredList[i].score > m) {
      m = this.filteredList[i].score;
    }
  }
  return m;
}

/** 及格率(>= 60 分) */
private get passRate(): number {
  if (this.totalCount <= 0) return 0;
  let pass: number = 0;
  for (let i: number = 0; i < this.filteredList.length; i++) {
    if (this.filteredList[i].score >= 60) pass = pass + 1;
  }
  return Math.round(pass * 100 / this.totalCount);
}

三个统计 getter 都基于 filteredList 计算,因此当筛选条件变化时,统计数据自动更新。这是响应式编程的一个显著优势:计算逻辑声明一次,数据变化时自动重新计算

9.2 统计概览 UI

Row() {
  Column() { Text(avgScore).fontColor('#1565C0')  Text('平均分') }
  Divider().height(24).width(1).color('#E0E0E0')    // 竖线分隔
  Column() { Text(maxScore).fontColor('#E65100')  Text('最高分') }
  Divider().height(24).width(1).color('#E0E0E0')    // 竖线分隔
  Column() { Text(passRate + '%').fontColor('#2E7D32')  Text('及格率') }
}

每个统计项使用 Column 垂直排列数字和标签,数字用大字号(20fp)和不同颜色突出显示,标签用小字号灰色辅助说明。


10. Divider 的另类用法:竖线分隔符

10.1 原理

Divider 默认是水平分割线(横跨父容器宽度)。但通过调整 heightwidth 的属性值,可以将其旋转为竖线:

Divider()
  .height(24)          // 竖线的高度(垂直长度)
  .width(1)            // 竖线的宽度(水平粗细)
  .color('#E0E0E0')    // 颜色

关键技巧:height 控制长度,width 控制粗细。这与水平分割线正好相反。

10.2 在统计概览中的应用

在统计概览区(弹性区 B),三个统计数字之间用竖线分隔:

  83       |      98       |      96%
 平均分    |     最高分    |     及格率

竖线分隔符比文字分隔符(如 “|”)更加清晰美观,而且颜色、粗细、长度都可以精确控制。

10.3 注意事项

使用 Divider 做竖线时,父容器必须是 Row(水平排列)。Divider 在 Row 中占据宽度为 1vp、高度为 24vp 的空间,与两侧的 Column 并列。


11. 班级筛选:ForEach 动态标签栏

11.1 标签栏实现

Row() {
  Text('全部')
    .fontColor(this.filterClass === '' ? '#FFD54F' : '#B3E5FC')
    .fontWeight(this.filterClass === '' ? FontWeight.Bold : FontWeight.Regular)
    .backgroundColor(this.filterClass === '' ? '#ffffff22' : 'transparent')
    .borderRadius(8)
    .onClick(() => { this.resetFilter(); })

  ForEach(CLASSES, (cls: ClassInfo) => {
    Text(cls.name)
      .fontColor(this.filterClass === cls.name ? '#FFD54F' : '#B3E5FC')
      .fontWeight(this.filterClass === cls.name ? FontWeight.Bold : FontWeight.Regular)
      .backgroundColor(this.filterClass === cls.name ? '#ffffff22' : 'transparent')
      .borderRadius(8)
      .margin({ left: 4 })
      .onClick(() => { this.setFilter(cls.name); })
  }, (cls: ClassInfo) => cls.name)
}

11.2 选中状态样式

当前选中的标签使用金色文字(#FFD54F)、粗体和半透明白色背景(#ffffff22);未选中的标签使用浅蓝色文字(#B3E5FC)、普通字重和透明背景。这种样式对比让当前筛选条件一目了然。

11.3 标签栏的响应性

当点击"高一(2)班"时:

  1. filterClass 变为 '高一(2)班'
  2. 标签栏重新渲染,"高一(2)班"获得选中样式,"全部"和"高一(1)班"恢复未选中样式
  3. filteredList 只返回高一(2)班的学生(8 人)
  4. 平均分、最高分、及格率基于这 8 人重新计算
  5. 被选中的学生 id 重置为 -1(收起所有展开的详情)

整个过程不需要手动操作任何 DOM,ArkUI 的响应式系统自动完成了所有步骤。


12. 分数等级系统:条件着色与标签映射

12.1 等级颜色映射

private scoreColor(s: number): string {
  if (s >= 90) return '#2E7D32';    // 优秀 → 绿色
  if (s >= 80) return '#1565C0';    // 良好 → 蓝色
  if (s >= 70) return '#E65100';    // 中等 → 橙色
  if (s >= 60) return '#8D6E63';    // 及格 → 棕色
  return '#C62828';                  // 不及格 → 红色
}

12.2 等级标签映射

private scoreLabel(s: number): string {
  if (s >= 90) return '优秀';
  if (s >= 80) return '良好';
  if (s >= 70) return '中等';
  if (s >= 60) return '及格';
  return '不及格';
}

12.3 在卡片中的应用

每个学生卡片的分数使用对应的等级颜色显示,右侧附加两字等级标签:

张明 ♂ 高一(1)班          92 优秀 ▼
杨磊 ♂ 高一(1)班          67 及格 ▼

颜色编码让分数高低一目了然——扫一眼颜色分布,就能大致判断班级的整体水平。绿色和蓝色越多,班级成绩越好;红色出现,说明有需要关注的学生。


13. 性能优化与最佳实践

13.1 @State 最小化原则

@State 是 ArkUI 响应式系统的核心,但滥用 @State 会导致性能问题——每次 @State 变量变化时,框架都会重新执行组件的 build() 方法,重新创建或更新 UI 树。如果 @State 变量变化过于频繁(比如每帧变化),就会造成严重的性能瓶颈。

因此,一个好的原则是:只将直接影响 UI 显示的数据标记为 @State,其他数据保持为普通变量、getter 计算属性或 const 常量。

本应用中只有两个 @State 变量:

@State private selectedId: number = -1;   // 需要 → UI 控制展开/收起
@State private filterClass: string = '';   // 需要 → UI 控制筛选标签

不需要 @State 的数据包括:

  • STUDENTS 常量数组——数据不会变化,声明为顶层 const
  • CLASSES 常量数组——同上
  • filteredList——这是一个 getter 计算属性,每次访问时根据 filterClass 重新计算,本身不存储状态
  • totalCountmaleCountfemaleCount——同样为 getter 计算属性
  • avgScoremaxScorepassRate——统计计算,基于 filteredList 实时计算

如果错误地把 avgScore 声明为 @State,那么每次筛选变化时,开发者需要手动 this.avgScore = newValue,不仅代码冗余,还容易遗漏。用 getter 计算属性,ArkUI 会自动追踪依赖关系,在依赖的 @State 变化时自动重新计算。

13.2 ForEach 的 key 函数

ForEach(this.filteredList, (stu, idx) => {
  // 学生卡片 UI
}, (stu: Student) => stu.id.toString())

ForEach 的第三个参数是 key 生成函数。key 是 ArkUI 框架追踪列表项的唯一标识。当列表数据变化时(比如筛选条件改变导致列表项增减),框架通过 key 判断:

  • key 存在的项 → 复用已有组件
  • key 不存在的项 → 销毁组件
  • key 新增的项 → 创建新组件

使用 stu.id.toString() 作为 key 是最佳实践——id 是每个学生的唯一标识,在整个数据集中不会重复,也不会随时间变化。如果使用 idx(索引)作为 key,当列表排序或筛选变化时,同一个 idx 可能对应不同的学生,导致组件状态错乱。

13.3 条件展开的性能考量

学生卡片的展开详情区域使用了 if 条件渲染,而不是 .visibility() 属性控制显隐:

if (this.selectedId === stu.id) {
  // 详情区域
  Column() {
    Divider().height(1).color('#E8E8E8')
    Row() { /* 年龄 + 专业 */ }
    Row() { /* 电话 + 学号 */ }
  }
}

if 条件渲染和 .visibility() 属性在功能上都可以实现"显示/隐藏",但二者的性能特征完全不同:

  • if 条件渲染:条件不满足时,组件完全不存在于组件树中,不占用任何内存,也不参与布局计算。条件从 false 变为 true 时,组件从头开始创建。
  • visibility(Hidden):组件始终存在于组件树中,只是不可见。它仍然占用布局空间,仍然参与布局计算。

对于"展开详情"这种交互——一次只展开一个卡片,99% 的时间里大多数卡片都是收起状态——使用 if 条件渲染可以显著减少组件数量,降低内存占用。

如果是一个"需要频繁显隐切换"的场景(比如动画效果),.visibility() 可能更合适。但在我们的学生管理应用中,if 条件渲染是最优选择。

13.4 避免在 build() 中做复杂计算

build() 方法应该只负责描述 UI 结构,不应该包含复杂的计算逻辑。本应用中的所有统计计算都封装在 getter 中:

// ✅ 在 getter 中计算
private get avgScore(): number {
  // 求和 → 求平均 → 返回
}

// ✅ 在 getter 中计算
private get passRate(): number {
  // 统计及格人数 → 计算百分比 → 返回
}

// build() 中只引用结果
build() {
  Text('' + this.avgScore)   // 直接使用计算好的值
}

这种分离的好处是:

  • build() 方法更简洁,只包含 UI 描述代码
  • 计算逻辑可以单独测试和复用
  • 当需要修改计算逻辑时,不需要改动 UI 代码

13.5 合理使用 Blank() 占位

在 Row 布局中,Blank() 是一个"饥饿"的占位组件——它会吃掉所有剩余空间。在行布局的末尾使用 Blank() 可以轻松实现"左对齐"效果:

Row() {
  Text('姓名')
  Blank()           // ← 吃掉中间所有剩余空间
  Text('分数:92')  // ← 被推到右端
}

这比设置 justifyContent(FlexAlign.SpaceBetween) 更加精确——Blank() 只占剩余空间,而 SpaceBetween 会将所有子组件均匀分布。

13.6 颜色常量的复用

分数等级颜色和标签通过两个函数 scoreColor()scoreLabel() 统一管理。这种集中管理的方式避免了在 UI 代码中分散的条件判断:

// 集中管理
private scoreColor(s: number): string {
  if (s >= 90) return '#2E7D32';
  if (s >= 80) return '#1565C0';
  // ...
}

// UI 中直接调用
Text('92').fontColor(this.scoreColor(92))  // → 绿色
Text('67').fontColor(this.scoreColor(67))  // → 棕色

当需要调整颜色方案时(比如把"优秀"从绿色改为金色),只需要修改 scoreColor() 一个地方,所有卡片同步更新。


14. 总结:从学生管理到通用数据应用

14.1 核心要点回顾

布局模式: 最外层 Column 使用 .width('100%').height('100%') 撑满全屏。内部子组件分为三种类型:用 height(px) 锁定的固定区(顶部标题栏 + 底部说明面板)、用 layoutWeight(flex) 分配剩余空间的弹性区(学生列表 + 统计概览 + 操作按钮)、以及由内容撑高的自适应区(同样也是底部说明面板)。这三种模式的混合使用,构成了 ArkUI 中构建数据管理界面的标准化方案。

Column()                           // 最外层:全屏
  .width('100%').height('100%')    // ★ 必须设置

Column() { /* 固定顶 */ }.height(68)           // 固定区
Column() { /* 列表 */ }.layoutWeight(1.0)      // 弹性区(50%)
Column() { /* 统计 */ }.layoutWeight(0.4)      // 弹性区(20%)
Column() { /* 按钮 */ }.layoutWeight(0.6)      // 弹性区(30%)
Column() { /* 说明 */ }                         // 固定区(内容撑高)

数据流模式: 用户操作 → 修改 @State 变量 → getter 计算属性自动重算 → UI 自动更新。这是一个闭环的单向数据流,不需要手动操作 DOM,不需要事件通知机制,ArkUI 的响应式系统自动完成所有中间步骤。

@State 变化 → getter 重算 → UI 自动更新
    ↑                           |
    用户操作 ←—— onClick ←——

功能实现速查表:

功能 技术 ArkTS 关键代码
标题栏 Column + Row Column().height(68)
筛选标签 ForEach + 条件样式 fontColor(cond?'#FFD54F':'#B3E5FC')
学生列表 Scroll + Column + Divider Scroll() { Column({space:0}) {} }
展开详情 @State + if 条件渲染 if (this.selectedId === stu.id) {}
统计计算 getter 计算属性 private get avgScore(): number {}
竖线分隔 Divider 变形 Divider().height(24).width(1)
分数着色 条件函数 scoreColor(s: number): string {}
操作按钮 Button + onClick Button('📊 统计分析').onClick(()=>{})
交互反馈 promptAction.showToast showToast({message:'...', duration:1500})

14.2 从学生管理到通用数据应用

本文的学生管理应用虽然只是一个示例,但其架构模式可以轻松迁移到任何数据管理类应用。只需要替换数据模型和界面文案,底层的架构完全不用动:

通讯录管理:将 Student 接口替换为 Contact 接口(字段改为姓名、电话、邮箱、分组),筛选条件改为"分组"或"首字母",统计改为"总人数/各分组人数"。Column + layoutWeight 的布局结构、Scroll + Divider 的列表渲染、@State + getter 的数据流——全部复用。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐