HarmonyOS 6.0 ArkUI 声明式 UI 实战 - 基于「今天空白」当前页面实现拆布局、条件渲染、弹层封装

系列文章 :HarmonyOS 6.0 实战开发 - 「今天空白」应用 第6篇 / 共30篇 发布时间 :2026-03-26 阅读时长 :19分钟 难度:(进阶)


本文导读

前两篇我们把 V2 状态管理和 Store 分层拆完了,这一篇终于轮到界面层。

这一篇不追求把 ArkUI 组件表都讲一遍,只按当前仓库里的 TodayPageEditSheetSettings 来看页面到底是怎么搭起来的。

这一篇继续基于「今天空白」项目真实代码,重点讲 4 件事:

  • Stack / Column / Row 怎么搭出一个层次清晰的页面

  • 条件渲染如何和状态管理配合

  • 编辑弹层、确认弹层这种覆盖层怎么写才不乱

  • 设计 Token 如何让样式统一,而不是每个组件手写一遍

对应文件主要是:

  • frontend/entry/src/main/ets/features/today/TodayPage.ets

  • frontend/entry/src/main/ets/features/today/EditSheet.ets

  • frontend/entry/src/main/ets/pages/Settings.ets

  • frontend/entry/src/main/ets/features/ui/UiTokens.ets


先看页面骨架: 一个 Stack 管三层

TodayPage.ets 的最外层不是 Column,而是 Stack():

复制代码
build() {
  Stack() {
    Column() {}
      .width('100%')
      .height('100%')
      .backgroundColor(UiTokens.ColorPageBg);
​
    Image($r('app.media.background'))
      .width('100%')
      .height('100%')
      .objectFit(ImageFit.Cover)
      .opacity(0.18);
​
    Column() {
      // 页面主内容
    }
    .padding(UiTokens.PagePadding)
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.SpaceBetween);
  }
}

按当前 TodayPage 的实现,这里实际形成了三层:

  • 第一层: 纯色背景

  • 第二层: 背景图

  • 第三层: 实际内容

为什么不用一个组件把背景和内容全写一起?

从当前代码看,之所以这样写,是因为同一个页面里后面还要继续叠编辑弹层和清空确认弹层。


Column 负责纵向结构,Row 负责局部对齐

TodayPage 主内容区本质上就两块:

  • 顶部标题栏

  • 中间内容卡片 + 底部操作区

所以外层主容器直接用 Column:

复制代码
Column() {
  Row() {
    // 顶栏
  }
​
  Column() {
    // 记录内容卡片
  }
​
  if (this.showBlank()) {
    Button('记录一件事')
  } else {
    Row() {
      Button('修改')
      Button('清空')
    }
  }
}
.justifyContent(FlexAlign.SpaceBetween)

对照当前页面,这样写最直接的结果是:

  • 页面结构一眼能看懂

  • 顶栏、内容区、操作区就是纵向排列

  • 操作区里两个按钮再用 Row 分开布局

至少在当前这个页面里,布局结构和视觉结构基本是一致的。


一个简单页面,为什么还要抽 UiTokens

UiTokens.ets 内容非常短:

复制代码
export class UiTokens {
  static readonly FontFamily: string = 'HarmonyOS Sans';
​
  static readonly ColorPageBg: string = '#F7F4F0';
  static readonly ColorSurface: string = '#F6F1EB';
  static readonly ColorSurfaceSoft: string = '#ECE6DF';
​
  static readonly ColorText: string = '#1E1B17';
  static readonly ColorTextMuted: string = '#5A524B';
  static readonly ColorDanger: string = '#B3382C';
​
  static readonly PagePadding: number = 24;
  static readonly RadiusM: number = 16;
  static readonly GapM: number = 12;
  static readonly ButtonHeight: number = 44;
}

如果只看当前仓库,抽 UiTokens 主要是为了解决这些已经发生的重复:

  • 同一个颜色在多个页面写了好几遍

  • 按钮高度不统一

  • 圆角大小每张卡片都不一样

  • 同一种视觉常量在多个页面反复出现

而当前项目把常用值收进 UiTokens 后,页面代码至少有两个直接变化:

1. 视觉语言统一

比如 TodayPageEditSheetSettings 都在复用:

  • UiTokens.ColorSurface

  • UiTokens.ButtonHeight

  • UiTokens.ButtonRadius

  • UiTokens.FontFamily

2. 页面代码更像"描述布局",而不是"填写常量"

对比一下:

复制代码
.backgroundColor(UiTokens.ColorSurface)
.borderRadius(UiTokens.RadiusM)
.padding(UiTokens.CardPadding)

和这种写法:

复制代码
.backgroundColor('#F6F1EB')
.borderRadius(16)
.padding(16)

前者更可读,因为它在表达设计语义;后者只是在堆数字。


条件渲染: 不要"改组件",要"改状态"

TodayPage 里最核心的 UI 分支是这两段。

第一段控制主文案:

复制代码
if (this.showBlank()) {
  Text('今天空白')
    .fontSize(30)
} else {
  Text(this.store.text)
    .fontSize(20)
    .lineHeight(28);
}

第二段控制操作区:

复制代码
if (this.showBlank()) {
  Button('记录一件事')
    .onClick(() => this.store.openEditor())
} else {
  Row() {
    Button('修改')
      .onClick(() => this.store.openEditor())
​
    Button('清空')
      .onClick(() => {
        this.showClearConfirm = true;
      })
  }
}

对照当前实现,这里最容易看清的是: 页面不是手动去改某个现成控件,而是根据状态直接走不同分支。

这会让代码具备两个优势:

  • 状态到界面的映射非常直观

  • 页面不用保存一堆"上一步 UI 长什么样"的过程变量

这和 TodayStore.status 驱动 TodayPage 渲染是同一套思路。


卡片式内容区为什么适合单独包一层 Column

中间展示记录内容的部分,项目里这样写:

复制代码
Column() {
  if (this.showBlank()) {
    Text('今天空白')
  } else {
    Text(this.store.text)
  }
​
  if (this.store.errorMessage && this.store.status !== 'editing') {
    Text(this.store.errorMessage)
      .fontSize(12)
      .fontColor(UiTokens.ColorDanger)
      .margin({ top: UiTokens.GapS });
  }
}
.padding(UiTokens.ModalCardPadding)
.backgroundColor(UiTokens.ColorSurface)
.borderRadius(UiTokens.RadiusL)
.width('100%')
.alignItems(HorizontalAlign.Start)

这一层的意义不只是"套个容器"而已,它本质上是在定义一个视觉块:

  • 有自己的背景色

  • 有自己的圆角

  • 有自己的内边距

  • 内容靠左对齐

  • 允许内部根据状态切换不同文本

从当前页面效果和代码结构看,这一层承担的是"内容卡片"而不是某一行文字。


编辑弹层: 当前项目就是用 Stack + 条件渲染 叠出来的

TodayPage 中编辑态的弹层就是这样实现的:

复制代码
if (this.store.status === 'editing') {
  Stack() {
    Column() {}
      .width('100%')
      .height('100%')
      .backgroundColor(UiTokens.ColorPrimary)
      .opacity(0.35);

    EditSheet({
      store: this.store,
      onSave: (text: string) => this.onSave(text)
    })
      .align(Alignment.Center);
  }
  .width('100%')
  .height('100%');
}

对照当前代码,这段实现可以直接拆成 4 层意思:

  • 弹层是否显示,由状态控制

  • 蒙层和弹窗内容都放在覆盖层里

  • 覆盖层整体再包一层 Stack

  • 具体表单内容抽到 EditSheet

之所以又拆出 EditSheet,是因为这个弹层本身已经是一个独立 UI 单元:

  • 有独立标题

  • 有输入区

  • 有取消/保存按钮

  • 还可能展示错误信息

从当前页面规模看,这样拆完以后 TodayPage 还能维持在"主页面骨架 + 条件分支"的层级。


EditSheet 的封装,和当前项目的页面职责是对齐的

EditSheet.ets 的结构非常克制:

复制代码
@ComponentV2
export struct EditSheet {
  @Require
  @Param store!: TodayStore;
  @Require
  @Param onSave!: (text: string) => void;

  build() {
    Column() {
      Text('记录')

      TextArea({
        text: this.store.draftText,
        placeholder: '写下你今天做过的一件事'
      })
        .onChange((value: string) => {
          this.store.draftText = value;
        });
    }
  }
}

这个组件没有再去拿 AppStore,也没有自己决定"保存后是否同步"。它只负责两件事:

  • 展示编辑界面

  • 采集编辑输入

结合当前项目,它更像是一个局部编辑视图,而不是一个再去管理应用逻辑的组件。


确认弹层和编辑弹层,为什么都用同一种覆盖模式

除了 EditSheet,页面里还有清空确认框:

复制代码
if (this.showClearConfirm) {
  Stack() {
    Column() {}
      .width('100%')
      .height('100%')
      .backgroundColor(UiTokens.ColorPrimary)
      .opacity(0.35);

    Column() {
      Text('确认清空?')
      Text('清空后:今天空白。')
      Row() {
        Button('取消')
        Button('清空')
      }
    }
    .padding(UiTokens.ModalCardPadding)
    .backgroundColor(UiTokens.ColorSurface)
    .borderRadius(UiTokens.RadiusM)
    .width('86%')
    .align(Alignment.Center);
  }
}

注意它和编辑弹层的结构几乎一致:

  • 外层覆盖式 Stack

  • 一层半透明蒙层

  • 中间一个居中的卡片内容区

这说明当前页面里两种覆盖层写法已经比较接近。哪怕还没抽公共 Modal,至少结构上已经统一了。

这种统一在当前仓库里的直接价值是:

  • 页面阅读成本低

  • 以后如果真要抽公共弹层,迁移成本会低一些

  • 不同弹层的视觉行为更一致

这比每次遇到弹窗就临时写一套布局强多了。


Settings 页面说明一个问题: ArkUI 页面也要讲"区域切块"

Settings.ets 虽然比首页复杂,但从当前代码看,整体还是按分区卡片在组织:

  • 顶部一个导航行

  • 背景预览一个卡片块

  • 账号区域一个卡片块

  • 同步区域一个卡片块

也就是说,哪怕表单项多起来,页面依然不是把所有控件一股脑往下堆,而是按业务分区切块。

放到当前页面里,这样分块最直接的结果是:

  • 视觉信息更清楚

  • 不同功能组之间边界明显

  • 阅读时更容易对上"背景 / 账号 / 同步"三块

这也是为什么本项目里 CardPaddingRadiusMGapL 这些 token 会被反复复用。不是为了"炫设计系统",而是为了把页面区域保持在统一节奏里。


从当前页面代码里能直接看出的 5 个实现事实

1. 首页和设置页都用了相同的背景层结构

  • 先铺纯色背景

  • 再叠背景图

  • 最上层再放内容

2. 首页主体就是"顶栏 + 内容卡片 + 操作区"

  • 顶栏用 Row

  • 页面主体用 Column

  • 操作区在空白态和已记录态之间切换

3. 编辑和确认都通过覆盖层追加,不会打散主页面骨架

  • 编辑态看 store.status === 'editing'

  • 清空确认看 showClearConfirm

4. UiTokens 已经承担了颜色、间距、圆角和按钮高度

  • TodayPageEditSheetSettings 都在用

5. EditSheet 已经从 TodayPage 中分离出来

  • 主页面保留骨架和状态分支

  • 编辑弹层负责局部输入和局部按钮


小结

这篇文章里,本小姐只做了一件事: 把当前仓库已经存在的页面结构拆开给你看。

  • TodayPageStack 组织背景层、内容层、覆盖层

  • EditSheet 承接编辑弹层

  • Settings 延续了相同背景与卡片分区风格

  • UiTokens 统一了颜色、间距、圆角和按钮高度

如果只按这个仓库当前实现来总结,那么最重要的不是抽象出一套大而全的 ArkUI 方法论,而是先看清页面结构、状态分支和样式层次现在到底是怎么对应上的,哼。( ̄^ ̄)

相关推荐
国医中兴3 小时前
云原生存储的实践与挑战:从容器到 Kubernetes
flutter·harmonyos·鸿蒙·openharmony
枫叶丹43 小时前
【HarmonyOS 6.0】Telephony Kit 新能力:精准获取卡槽ID与SIM卡对应关系
开发语言·华为·harmonyos
国医中兴3 小时前
ClickHouse 生态系统的深度解析:从核心到周边
flutter·harmonyos·鸿蒙·openharmony
BackCatK Chen3 小时前
2026国产科技技术全景解析:从芯片到系统的全栈自主可控路径
科技·嵌入式·业界资讯·鸿蒙·国产科技
国医中兴3 小时前
ClickHouse 在高并发写入场景下的性能优化实践
flutter·harmonyos·鸿蒙·openharmony
Amctwd3 小时前
【Android】将 html 打包为 apk
android·html·harmonyos
希望上岸的大菠萝3 小时前
HarmonyOS 6.0 V2 状态管理实战(下)- 基于 AppStore + TodayStore 拆当前项目的 Store 分层
华为·harmonyos·鸿蒙
黑鲨吃西瓜3 小时前
鸿蒙开发中V2状态管理的使用(下)
harmonyos·鸿蒙·deveco studio
江湖有缘5 小时前
基于开发者空间部署Eigenfocus项目管理工具【华为开发者空间】
运维·服务器·华为