HarmonyOS 校园应用系列之ArkTS 表单工程:单词创建页的 Section 分组与图标化类型选择
HarmonyOS 校园应用系列之ArkTS 表单工程:单词创建页的 Section 分组与图标化类型选择
源码位置:
entry/src/main/ets/pages/Func1Tab.ets(215 行)
核心主题:表单布局 + Section 分组卡片 + 图标化类型选择 + 优先级三档 + Toggle 开关 + 快捷模板 + 提交校验
技术关键词:TextInput、TextArea、Toggle、Scroll、@Builder、条件样式、表单校验
一、页面定位与功能概述
单词创建页是整个应用的数据入口——用户在这里通过填写表单的方式创建新的学习项目。页面从上到下分为六个功能区:固定标题栏(创建 + 副标题 + 剪贴板图标)、基本信息区(标题输入 + 描述文本 + 图片/链接附件)、选择类型区(四宫格图标卡片)、优先级区(低/中/高三档)、设置区(提醒通知 Toggle + 隐私设置)、快捷模板区(三个模板入口)和提交按钮。
与首页的信息展示角色不同,本页是纯粹的 数据录入型页面——它包含了 TextInput、TextArea、Toggle 等多种受控表单组件,是学习 ArkUI 表单开发的最佳实战案例之一。页面整体采用 “Section 分组卡片” 布局:每个功能区用白色圆角卡片包裹,卡片之间有 14vp 的间距(Column({ space: 14 })),视觉上清晰分组。


二、页面头部与导航设计
页面顶部是一个固定的标题栏(Header @Builder),包含页面名称、副标题和右侧操作图标,标题栏下方是可滚动的表单区:
// Func1Tab.ets 根布局
build() {
Column() {
this.Header()
Scroll() {
Column({ space: 14 }) {
this.FormCard()
this.TypeCard()
this.PriorityCard()
this.SettingCard()
this.TemplateCard()
this.SubmitBtn()
Blank().height(this.safeBottom + 20)
}
.width('100%').padding({ left: D.pad, right: D.pad, top: 6 })
}
.layoutWeight(1).scrollBar(BarState.Off).align(Alignment.Top)
}
.width('100%').height('100%').backgroundColor(C.bg)
}
// Header @Builder — 固定标题栏
@Builder
Header() {
Row({ space: 12 }) {
Column({ space: 2 }) {
Text('创建').fontSize(20).fontWeight(FontWeight.Bold).fontColor(C.text)
Text('填写信息快速创建').fontSize(10).fontColor(C.textDim)
}.alignItems(HorizontalAlign.Start)
Blank()
Row() { Text('📋').fontSize(18) }
.width(36).height(36).backgroundColor(C.cardSoft).borderRadius(D.rSm).justifyContent(FlexAlign.Center)
}
.width('100%').height(this.safeTop + 60)
.padding({ top: this.safeTop, left: D.pad, right: D.pad })
.backgroundColor(C.card).alignItems(VerticalAlign.Bottom)
.border({ width: { bottom: 1 }, color: C.stroke })
}
标题栏使用 Row + Blank() 布局——左侧是"创建"标题(20fp 粗体)与副标题"填写信息快速创建"(10fp)组成的双行 Column,右侧是剪贴板图标。Blank() 是 ArkUI 中实现弹性空间分配的利器:它会自动占据 Row 中所有未被显式设置宽度的剩余空间。
标题栏的高度设为 this.safeTop + 60,配合 padding({ top: this.safeTop }) 与 alignItems(VerticalAlign.Bottom)——内容整体沉底对齐,状态栏区域自然留白;backgroundColor(C.card) 白色底 + 底部 1vp C.stroke 描边让它与下方滚动区形成"固定栏 + 滚动内容"的经典结构。右侧的剪贴板图标是 📋 emoji 文本(18fp),装在 36×36vp、C.cardSoft 底色、D.rSm 圆角的容器里——它暗示了 “快捷粘贴” 功能,在实际产品中,点击它可以读取系统剪贴板内容并自动填充到表单字段中。这种微交互细节能显著提升高频录入场景下的用户体验。
三、FormCard 基本信息区
FormCard 是最复杂的表单区域,包含区块头部、标题输入、描述输入和底部工具栏:
@Builder
FormCard() {
Column({ space: 12 }) {
// 区块头部
Row() {
Text('基本信息').fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text)
Blank()
Text('✕').fontSize(16).fontColor(C.textDim)
}.width('100%')
// 标题输入
Column({ space: 6 }) {
Text('标题').fontSize(12).fontColor(C.textSub)
TextInput({ placeholder: '请输入标题', text: this.inputTitle })
.onChange((v: string) => { this.inputTitle = v; })
.height(44).backgroundColor(C.cardSoft).borderRadius(D.rSm).placeholderColor(C.textDim)
}.alignItems(HorizontalAlign.Start).width('100%')
// 描述输入
Column({ space: 6 }) {
Text('描述').fontSize(12).fontColor(C.textSub)
TextArea({ placeholder: '请输入详细描述...', text: this.inputDesc })
.onChange((v: string) => { this.inputDesc = v; })
.height(90).backgroundColor(C.cardSoft).borderRadius(D.rSm).placeholderColor(C.textDim)
}.alignItems(HorizontalAlign.Start).width('100%')
// 底部工具栏
Row({ space: 8 }) {
Text('📷 图片').fontSize(12).fontColor(C.textSub)
.padding({ left: 10, right: 10, top: 6, bottom: 6 })
.backgroundColor(C.cardSoft).borderRadius(D.rSm)
Text('🔗 链接').fontSize(12).fontColor(C.textSub)
.padding({ left: 10, right: 10, top: 6, bottom: 6 })
.backgroundColor(C.cardSoft).borderRadius(D.rSm)
Blank()
Text(`${this.inputTitle.length}/50`).fontSize(10).fontColor(C.textDim)
}.width('100%')
}
.width('100%').padding(14).backgroundColor(C.card).borderRadius(D.rMd)
.border({ width: 1, color: C.stroke })
}
3.1 TextInput 标题输入
TextInput({ placeholder: '请输入标题', text: this.inputTitle })
.onChange((v: string) => { this.inputTitle = v; })
.height(44).backgroundColor(C.cardSoft).borderRadius(D.rSm).placeholderColor(C.textDim)
关键属性解读:
placeholder:占位提示文字,在输入框为空时显示text: this.inputTitle:双向绑定——初始值来自 @State 变量inputTitle.onChange():每次文字变化时更新this.inputTitle,实现受控组件模式.height(44):44vp 是单行输入框的标准高度,符合触控目标最小尺寸要求.backgroundColor(C.cardSoft):极浅灰色背景(#F4F6FB),比纯白更有"可输入"的暗示感.placeholderColor(C.textDim):占位文字用最浅的辅助色,与真实输入内容形成区分
3.2 TextArea 多行描述
TextArea({ placeholder: '请输入详细描述...', text: this.inputDesc })
.onChange((v: string) => { this.inputDesc = v; })
.height(90).backgroundColor(C.cardSoft).borderRadius(D.rSm).placeholderColor(C.textDim)
TextArea 与 TextInput 的 API 设计保持一致(都有 placeholder/text/onChange),区别在于:
- TextArea 默认支持多行输入,高度设为 90vp 可容纳约 3~4 行文字
- 在实际产品中通常还会添加
.maxLines(5)限制最大行数
3.3 底部附件工具栏
底部工具行包含两个附件按钮(图片/链接)和一个字数统计器:
附件按钮 是 📷 图片、🔗 链接 两个 emoji 前缀的 Text 标签,配 C.cardSoft 浅灰底色和 D.rSm 圆角、left: 10, right: 10, top: 6, bottom: 6 的内边距。这种 “图标+文字+色块” 的组合让按钮看起来像可点击的标签(Tag Button),比纯文字或纯图标按钮更醒目。
字数统计器 `${this.inputTitle.length}/50` 实时显示当前已输入字符数和上限。这是表单 UX 的重要模式——用户不需要自己数是否超限,系统实时反馈消除了不确定性。50 字的限制通过 UI 暗示而非硬性截断,体现了 “引导优于限制” 的设计哲学。
四、TypeCard 图标化类型选择
类型选择区采用四宫格卡片布局,每个选项由 emoji 图标容器 + 文字标签组成:
@Builder
TypeCard() {
Column({ space: 12 }) {
Text('选择类型').fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text).width('100%')
Row() {
ForEach(this.types, (item: OptionItem, idx: number) => {
Column({ space: 6 }) {
Row() { Text(item.icon).fontSize(24) }
.width(48).height(48).borderRadius(D.rMd).justifyContent(FlexAlign.Center)
.backgroundColor(this.selectedType === idx ? C.primarySoft : C.cardSoft)
.border({ width: this.selectedType === idx ? 2 : 0, color: C.primary })
Text(item.name).fontSize(10)
.fontColor(this.selectedType === idx ? C.primary : C.textDim)
.fontWeight(this.selectedType === idx ? FontWeight.Bold : FontWeight.Normal)
}.layoutWeight(1)
.onClick(() => { this.selectedType = idx; })
}, (item: OptionItem) => item.name)
}.width('100%')
}
.width('100%').padding(14).backgroundColor(C.card).borderRadius(D.rMd)
.border({ width: 1, color: C.stroke })
}
选中态的视觉反馈 通过多个维度同时变化:
- 图标容器背景色:
C.cardSoft浅灰 →C.primarySoft浅蓝 - 图标容器边框:0vp 无边框 → 2vp
C.primary蓝色描边 - 文字颜色与字重:
C.textDim常规 →C.primary粗体 - 这几个维度的组合让选中状态极其明显,即使用户在户外强光下也能清晰识别
OptionItem 接口:
interface OptionItem { name: string; icon: string; }
四个类型选项分别是 类型一 📝、类型二 🎯、类型三 ⭐、类型四 📊——icon 字段是 emoji 字符串而非图片资源,直接用 Text(item.icon).fontSize(24) 渲染,装在 48×48vp、D.rMd 圆角的容器里。相比 $r('app.media.xxx') 引用本地图片,emoji 方案零资源文件、零加载开销,且天然跨分辨率,非常适合原型和 Demo 阶段。
五、PriorityCard 优先级选择
优先级选择区提供了三个档次:低、中、高。默认选中"中"(selectedPriority 初始值为 1):
@Builder
PriorityCard() {
Column({ space: 12 }) {
Row() {
Text('优先级').fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text)
Blank()
Text(this.priorities[this.selectedPriority]).fontSize(13).fontColor(C.primary).fontWeight(FontWeight.Bold)
}.width('100%')
Row({ space: 6 }) {
ForEach(this.priorities, (p: string, idx: number) => {
Text(p).fontSize(13)
.fontColor(this.selectedPriority === idx ? '#FFFFFF' : C.textSub)
.backgroundColor(this.selectedPriority === idx ? (idx === 2 ? C.danger : idx === 1 ? C.warn : C.ok) : C.cardSoft)
.borderRadius(D.rSm).padding({ left: 16, right: 16, top: 8, bottom: 8 })
.onClick(() => { this.selectedPriority = idx; })
}, (p: string) => p)
}.width('100%')
}
.width('100%').padding(14).backgroundColor(C.card).borderRadius(D.rMd)
.border({ width: 1, color: C.stroke })
}
"中"为默认选中项的产品逻辑:在大多数任务管理场景中,大部分任务的优先级都是"中等"——既不紧急也不重要到需要立即处理。将"中"作为默认值减少了用户的操作步骤(80% 的情况下无需修改)。这符合 “优化常见路径” 的 UX 原则。
选中态的颜色语义(红绿灯配色):
- "低"选中时:绿底白字(
C.ok背景 + 白色文字)——从容、无压力 - "中"选中时:橙底白字(
C.warn背景 + 白色文字)——常规优先级 - "高"选中时:红底白字(
C.danger背景 + 白色文字)——强烈警示
未选中时统一为 C.cardSoft 浅灰底 + C.textSub 灰字。这种 “颜色=紧急程度” 的红黄绿三色体系与交通信号灯的心智模型一致,用户无需阅读文字就能感知优先级高低。卡片头部右侧还有一行 Text(this.priorities[this.selectedPriority]) 实时回显当前选中的档位(13fp C.primary 粗体)。
六、SettingCard 设置区:Toggle 开关与隐私入口
设置区是一张包含两行的卡片——第一行是提醒通知 Toggle 开关,第二行是隐私设置跳转入口,中间用 Divider 分隔:
@Builder
SettingCard() {
Column({ space: 0 }) {
Row({ space: 12 }) {
Row() { Text('🔔').fontSize(16) }
.width(32).height(32).backgroundColor(C.cardSoft).borderRadius(D.rSm).justifyContent(FlexAlign.Center)
Column({ space: 2 }) {
Text('提醒通知').fontSize(14).fontColor(C.text)
Text('开启后将推送提醒').fontSize(11).fontColor(C.textDim)
}.alignItems(HorizontalAlign.Start).layoutWeight(1)
Toggle({ type: ToggleType.Switch, isOn: this.remindOn })
.selectedColor(C.primary)
.onChange((on: boolean) => { this.remindOn = on; })
}.width('100%').padding({ top: 12, bottom: 12 })
Divider().color(C.stroke)
Row({ space: 12 }) {
Row() { Text('🔒').fontSize(16) }
.width(32).height(32).backgroundColor(C.cardSoft).borderRadius(D.rSm).justifyContent(FlexAlign.Center)
Column({ space: 2 }) {
Text('隐私设置').fontSize(14).fontColor(C.text)
Text('仅自己可见').fontSize(11).fontColor(C.textDim)
}.alignItems(HorizontalAlign.Start).layoutWeight(1)
Text('›').fontSize(22).fontColor(C.textDim)
}.width('100%').padding({ top: 12, bottom: 12 })
}
.width('100%').padding({ left: 14, right: 14 }).backgroundColor(C.card).borderRadius(D.rMd)
.border({ width: 1, color: C.stroke })
}
Toggle 组件配置:
type: ToggleType.Switch:iOS 风格的滑动开关(还有ToggleType.Checkbox和ToggleType.Button可选)isOn: this.remindOn:双向绑定 @State 变量.selectedColor(C.primary):开启状态的轨道颜色为主题蓝色
信息架构:开关左侧有 🔔 emoji 图标(16fp,装在 32×32vp 的 C.cardSoft 容器里)+ 双行文字(标题 14fp + 说明 11fp),右侧是 Toggle 控件。这种 “左说明右操作” 的布局是 iOS Settings 应用中的标准模式——用户从左到右扫视时先理解功能含义,再决定是否开启。第二行"隐私设置"用 › 箭头暗示可跳转,与 Toggle 行形成"开关型设置 + 跳转型设置"的对比。
六点五、TemplateCard 快捷模板与 SubmitBtn 提交按钮
快捷模板区提供三个模板入口,点击后 Toast 提示模板名称:
@Builder
TemplateCard() {
Column({ space: 10 }) {
Text('快捷模板').fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text).width('100%')
ForEach(this.templates, (item: QuickTemplate) => {
Row({ space: 10 }) {
Row() { Text('📋').fontSize(18) }
.width(32).height(32).backgroundColor(C.primarySoft).borderRadius(D.rSm).justifyContent(FlexAlign.Center)
Column({ space: 2 }) {
Text(item.title).fontSize(13).fontWeight(FontWeight.Medium).fontColor(C.text)
Text(item.desc).fontSize(10).fontColor(C.textDim)
}.alignItems(HorizontalAlign.Start).layoutWeight(1)
Text('›').fontSize(18).fontColor(C.textDim)
}.width('100%')
.onClick(() => { promptAction.showToast({ message: item.title }); })
}, (item: QuickTemplate) => item.title)
}
.width('100%').padding(14).backgroundColor(C.card).borderRadius(D.rMd)
.border({ width: 1, color: C.stroke })
}
三个模板分别是"快速创建(使用默认模板)"、“高级模式(自定义所有字段)”、“批量导入(从文件导入)”。注意模板行的图标容器用的是 C.primarySoft 浅蓝底(区别于设置区的 C.cardSoft 浅灰底),暗示这是"可快捷填充"的操作入口。
提交按钮区是表单的收尾,按钮文案和颜色随标题输入状态联动:
@Builder
SubmitBtn() {
Column({ space: 8 }) {
Button(this.inputTitle.length > 0 ? '✓ 提交创建' : '请填写标题')
.width('100%').height(48)
.backgroundColor(this.inputTitle.length > 0 ? C.primary : C.cardSoft).fontColor('#FFFFFF')
.fontSize(16).fontWeight(FontWeight.Bold).borderRadius(D.rMd)
.onClick(() => {
if (this.inputTitle.length === 0) { promptAction.showToast({ message: '请输入标题' }); return; }
promptAction.showToast({ message: '创建成功!' });
this.inputTitle = ''; this.inputDesc = '';
})
Text('提交即表示同意相关条款').fontSize(9).fontColor(C.textDim).width('100%').textAlign(TextAlign.Center)
}.width('100%')
}
状态驱动的按钮设计:标题为空时按钮显示"请填写标题"、C.cardSoft 灰底;一旦 inputTitle.length > 0,按钮变为"✓ 提交创建"、C.primary 蓝底。用户无需点击就能从按钮外观预判表单是否可提交——这是 “防错优于纠错” 的 UX 原则。点击后若标题为空会 Toast "请输入标题"并 return;成功则 Toast "创建成功!"并清空 inputTitle、inputDesc 两个字段,为连续录入做准备。按钮下方还有一行 9fp 的"提交即表示同意相关条款"法律提示文案。
七、@State 状态管理全景
本页共有 5 个 @State 变量,各司其职:
@State inputTitle: string = ''; // 标题输入
@State inputDesc: string = ''; // 描述内容
@State selectedType: number = 0; // 选中的类型索引
@State selectedPriority: number = 1; // 优先级(0=低, 1=中, 2=高),默认"中"
@State remindOn: boolean = true; // 提醒开关,默认开启
每个变量只负责一个表单字段的中间状态,没有冗余的派生状态。这种 “一变量一职责” 的原则让代码易于理解和调试——当某个字段行为异常时,可以立即定位到对应的 @State 变量和它的 onChange 回调。
而三个静态选项数组(types、priorities、templates)声明为 private 普通成员而非 @State——它们是展示型常量数据,不参与状态变化,避免无意义的观察开销。
数据流方向清晰单向:用户操作 → onChange 更新 @State → ArkUI diff 重绘相关节点。没有隐式的跨组件状态共享,所有状态变化都可追踪。

八、表单开发的工程化最佳实践总结
单词创建页虽然只有 215 行代码,但它完整展示了 HarmonyOS ArkUI 表单开发的全流程最佳实践,值得作为模板复用到任何需要表单的项目中:
1. 数据模型先行:所有数据结构先定义 interface(OptionItem, QuickTemplate),再使用。这让 IDE 能提供完整的类型补全和编译期错误检查。
2. 状态集中管理:5 个 @State 变量各司其职,每个变量只负责一个表单字段的中间状态。没有冗余的派生状态。
3. 样式抽离复用:Theme.ets 的 C(颜色)/D(尺寸)常量类消除了大量重复的硬编码字面量。
4. Section 分组卡片:每个功能区独立成卡片,视觉上清晰分组。卡片间距统一为 14vp(Column({ space: 14 })),内部 padding 统一为 14vp。
5. 条件样式三元表达式:this.selectedType === idx ? A : B 模式贯穿整个页面,用于控制选中/未选中的视觉差异。
6. 渐进式信息 disclosure:页面从上到下按照"核心信息 → 分类选择 → 优先级设定 → 辅助设置 → 快捷模板 → 提交"的逻辑顺序排列,符合用户创建一条记录时的自然思维流程。
九、表单校验与提交流程的工程化思考
当前实现中表单已有完整的前端校验与提交反馈(见 SubmitBtn 的 onClick),但从架构角度可以预见到接入后端后的完整提交流程:
第一步:前端校验 —— 在 onClick 回调中检查必填字段(源码已实现标题校验):
// SubmitBtn 中的真实校验逻辑
.onClick(() => {
if (this.inputTitle.length === 0) { promptAction.showToast({ message: '请输入标题' }); return; }
promptAction.showToast({ message: '创建成功!' });
this.inputTitle = ''; this.inputDesc = '';
})
第二步:数据组装 —— 将 @State 变量组装为 API 需要的数据结构:
// 伪代码示例
const payload = {
title: this.inputTitle,
desc: this.inputDesc,
type: this.types[this.selectedType].name,
priority: this.priorities[this.selectedPriority],
remindOn: this.remindOn,
};
第三步:异步请求 —— 使用 HarmonyOS 的 http 模块发送 POST 请求,处理成功/失败/网络异常三种情况。
第四步:状态重置或路由跳转 —— 成功后清空文本字段(源码已实现 inputTitle、inputDesc 置空)并显示成功 Toast,或跳转到详情页。
这种 “校验→组装→请求→反馈” 的四步提交流程是表单开发的标准模式,适用于任何需要数据录入的场景。

十、TextInput 与 TextArea 的键盘适配
在真机上,当 TextInput 或 TextArea 获得焦点时,系统键盘会从底部弹出,可能遮挡输入框或提交按钮。HarmonyOS 提供了多种解决方案:
.expandSafeArea([SafeAreaType.KEYBOARD])— 让输入框所在容器自动避开键盘区域- 将输入区域放在 Scroll 内部 — 键盘弹出时自动调整 Scroll 的可滚动范围
- 监听键盘高度变化 — 通过
window.on('keyboardHeightChange')动态调整 padding
对于本页这种多字段表单,方案 2(Scroll 包裹)是最简单且体验最好的选择——它不需要手动计算任何偏移量,框架会自动处理键盘避让逻辑。事实上本页源码正是这么做的:Header 固定在顶部,其余表单内容全部包裹在 Scroll 中,键盘弹出时用户仍可滚动到被遮挡的输入框。
十一、Toggle 组件的深入分析
SettingCard 中的 Toggle 开关使用了 ToggleType.Switch 类型(iOS 风格滑动开关)。HarmonyOS 还提供了另外两种 Toggle 样式:
| 类型 | 枚举值 | 适用场景 |
|---|---|---|
| 滑动开关 | ToggleType.Switch |
设置页开关(iOS 风格) |
| 复选框 | ToggleType.Checkbox |
多选列表、协议勾选 |
| 按钮 | ToggleType.Button |
独立切换按钮 |
Toggle 的 onChange 回调返回 on: boolean 参数,表示开关的最新状态。注意这个参数值已经是最新的——不需要再读取 this.remindOn 来获取状态。但在回调内部赋值 this.remindOn = on 后,ArkUI 会触发依赖 remindOn 的所有表达式重新求值,从而更新 UI。本例中 .selectedColor(C.primary) 是固定值,开关状态的视觉变化由组件内部根据 isOn 自动处理。
Toggle 与 Checkbox 的选择原则:当选项是"开/关"两种互斥状态时用 Toggle 更直观;当选项是"选中/未选中"且可能有多个同类项时用 Checkbox 语义更准确。本例中"提醒通知"明显是二态开关,Toggle 是正确选择。

十二、表单重置与连续录入体验
在实际产品中,用户可能需要连续创建多条记录(如批量添加单词)。此时提交后的 表单重置逻辑 至关重要。本页源码的实现是:提交成功后仅清空 inputTitle 和 inputDesc 两个文本字段,selectedType、selectedPriority、remindOn 保持用户当前选择不变——因为连续录入时类型、优先级、提醒设置大概率与上一条相同,保留它们能显著减少重复操作。重置时可以配合 animateTo() API 添加淡入淡出动画,让用户体验到"已准备好接受下一条输入"的心理暗示。
十三、页面间数据传递的设计考量
创建页的数据最终需要传递给其他页面消费(如首页的项目列表、统计页的学习数据)。在 HarmonyOS 中有几种常见的跨页面数据传递方式:路由参数(router params) 适用于一次性数据传递;AppStorage 全局存储适用于需要多页面共享的运行时数据;Preferences/RelationalStore 持久化存储适用于跨应用生命周期保留的数据。对于智能管理应用这种规模的项目,推荐使用 AppStorage 作为运行时内存数据库 + Preferences 作为持久化存储的双层架构——AppStorage 保证页面间的实时数据同步,Preferences 保证应用重启后数据不丢失。
更多推荐




所有评论(0)