HarmonyOS 6.0 ArkUI 声明式 UI 实战 - 基于「今天空白」当前页面实现拆布局、条件渲染、弹层封装
本文是HarmonyOS 6.0开发实战系列的第6篇,重点讲解「今天空白」应用的界面层实现。文章基于真实项目代码,分析了页面布局的关键技术:使用Stack管理三层结构(背景层、内容层、覆盖层),通过Column/Row构建页面骨架,采用条件渲染与状态管理配合实现UI切换。特别强调了UiTokens在统一视觉样式中的作用,以及EditSheet等组件的合理封装方式。文章还对比了编辑弹层和确认弹层的实
系列文章:HarmonyOS 6.0 实战开发 - 「今天空白」应用 第6篇 / 共30篇 发布时间:2026-03-26 阅读时长:19分钟 难度:(进阶)
本文导读
前两篇我们把 V2 状态管理和 Store 分层拆完了,这一篇终于轮到界面层。
这一篇不追求把 ArkUI 组件表都讲一遍,只按当前仓库里的 TodayPage、EditSheet、Settings 来看页面到底是怎么搭起来的。
这一篇继续基于「今天空白」项目真实代码,重点讲 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. 视觉语言统一
比如 TodayPage、EditSheet、Settings 都在复用:
-
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 虽然比首页复杂,但从当前代码看,整体还是按分区卡片在组织:
-
顶部一个导航行
-
背景预览一个卡片块
-
账号区域一个卡片块
-
同步区域一个卡片块
也就是说,哪怕表单项多起来,页面依然不是把所有控件一股脑往下堆,而是按业务分区切块。
放到当前页面里,这样分块最直接的结果是:
-
视觉信息更清楚
-
不同功能组之间边界明显
-
阅读时更容易对上“背景 / 账号 / 同步”三块
这也是为什么本项目里 CardPadding、RadiusM、GapL 这些 token 会被反复复用。不是为了“炫设计系统”,而是为了把页面区域保持在统一节奏里。
从当前页面代码里能直接看出的 5 个实现事实
1. 首页和设置页都用了相同的背景层结构
-
先铺纯色背景
-
再叠背景图
-
最上层再放内容
2. 首页主体就是“顶栏 + 内容卡片 + 操作区”
-
顶栏用
Row -
页面主体用
Column -
操作区在空白态和已记录态之间切换
3. 编辑和确认都通过覆盖层追加,不会打散主页面骨架
-
编辑态看
store.status === 'editing' -
清空确认看
showClearConfirm
4. UiTokens 已经承担了颜色、间距、圆角和按钮高度
-
TodayPage、EditSheet、Settings都在用
5. EditSheet 已经从 TodayPage 中分离出来
-
主页面保留骨架和状态分支
-
编辑弹层负责局部输入和局部按钮
小结
这篇文章里,本小姐只做了一件事: 把当前仓库已经存在的页面结构拆开给你看。
-
TodayPage用Stack组织背景层、内容层、覆盖层 -
EditSheet承接编辑弹层 -
Settings延续了相同背景与卡片分区风格 -
UiTokens统一了颜色、间距、圆角和按钮高度
如果只按这个仓库当前实现来总结,那么最重要的不是抽象出一套大而全的 ArkUI 方法论,而是先看清页面结构、状态分支和样式层次现在到底是怎么对应上的,哼。( ̄^ ̄)
更多推荐

所有评论(0)