鸿蒙原生应用 ArkTS 表单工程:志愿活动创建页的优先级三色与动态按钮
鸿蒙原生应用 ArkTS 表单工程:志愿活动创建页的优先级三色与动态按钮
App 17「志愿服务」活动 Tab(Func1Tab),是活动创建的表单页。整页由 7 个 @Builder 块拼出 Header + 基本信息 + 类型选择 + 优先级选择 + 提醒设置 + 快捷模板 + 提交按钮(动态文案),是 ArkUI 声明式表单的"模板范本"。与 App 13 歌曲创建页几乎同源——是"模板换肤适配新业务"的最佳示例。本篇基于
17-volunteer/entry/src/main/ets/pages/Func1Tab.ets(约 215 行)逐段拆解,附 4 张实机截图。
一、整体结构:6 个 @Builder 的表单漏斗
Func1Tab 的骨架与 App 13 歌曲创建页几乎完全相同——"Header + 6 个表单卡片 + 提交按钮"的固定漏斗:
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)
}
6 个块按"必填 → 必选 → 选填 → 操作"的漏斗叙事展开:
- FormCard — 标题/描述/图片链接/字数统计(必填区)
- TypeCard — 4 种活动类型图标网格(必选)
- PriorityCard — 优先级三色(低/中/高)
- SettingCard — 提醒通知 Toggle + 隐私设置入口
- TemplateCard — 3 个快捷模板(快速创建/高级模式/批量导入)
- SubmitBtn — 提交按钮(动态文案 + 早返回校验)
"Scroll 容器 + 固定 Header" 是表单页的标准布局——Header 不滚动(用户填到一半还能看到"创建"标题),表单长内容可滚动。


二、Header:固定标题 + 右侧图标
Header 单行结构(与 App 13 同构),是"页面身份"标识:
@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 })
}
height(this.safeTop + 60) + padding({ top: this.safeTop }) + alignItems(VerticalAlign.Bottom) 三件套实现"安全区顶部让出 + 内容靠底对齐"——height 等于状态栏+60vp 标题区,padding.top 让出状态栏区域,alignItems(Bottom) 让标题/图标贴底对齐。这是 ArkUI 沉浸式 Header 的标准写法。
border({ width: { bottom: 1 }, color: C.stroke }) 底部 1vp 浅灰分隔线,把 Header 与下方 FormCard 视觉分隔——比给下方 FormCard 加 border-top 简洁(避免重复边框)。
与 App 13 几乎完全相同——只有 2 处差异:① 文案"创建"+"填写信息快速创建" vs App 13 略有不同;② 主题色背景(虽然 Header 是白底 + 边框,但 C.card / C.stroke 都是同一套主题色变量,红色 #EF4444 间接通过 C.primary 体现在按钮/选中态)。
三、FormCard:基本输入 + 字数统计 + 附件标签
FormCard 是表单核心输入区——标题 TextInput、描述 TextArea、图片/链接"伪按钮"、字数统计:
@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 })
}
TextInput + TextArea + 字数统计三件套:
TextInput({ placeholder: '请输入标题', text: this.inputTitle })
.onChange((v: string) => { this.inputTitle = v; })
.height(44).backgroundColor(C.cardSoft).borderRadius(D.rSm).placeholderColor(C.textDim)
onChange 双向绑定:text: this.inputTitle(受控)+ onChange(写回 @State)—— ArkUI 声明式表单的标准模式。backgroundColor(C.cardSoft).borderRadius(D.rSm) 形成"软灰底"输入框,比纯白底更聚焦用户视线。
${this.inputTitle.length}/50 字数统计——@State inputTitle 变化自动重渲染,用户每敲一个字就更新"0/50→1/50→2/50"。"50 字"上限暗示活动标题要精简。
"图片/链接"伪按钮:📷 图片 + 🔗 链接 两个 Text 套 padding + 背景色做成"标签型按钮"——Blank() 撑开,把字数统计推到右侧。"文字 + padding + 背景"是 ArkUI 轻量按钮的标准模式。
与 App 13 完全同源——inputTitle/inputDesc 两个 @State 双向绑定,字数统计实时同步。"同样的表单骨架换主题色"——C.cardSoft 变量决定了所有"软灰底"是浅红色调(在红色主题下略偏暖)。
四、TypeCard:4 选 1 类型图标网格
TypeCard 是 4 种活动类型(📝/🎯/⭐/📊)的等宽图标网格:
@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.primarySoft浅红 vsC.cardSoft浅灰 - 边框:2vp 主色(红)vs 无边框
- 文字:主色加粗 vs 灰色常规
@State selectedType: number = 0 默认选中"类型一"——onClick(() => { this.selectedType = idx; }) 更新选中。
C.primarySoft: '#FEE8E8' 浅红 vs C.primary: '#EF4444' 主红——浅红背景 + 红边 + 红字 = "同色系深浅"高亮。**"选中不变色、只加深"**是本系列单色主题下的标准手法(与 App 15 的"四色分类"是相反思路——App 15 4 分类颜色不同,必须用同色深浅表达选中;App 17 单色主题,直接用主题色深浅即可)。
五、PriorityCard:三色优先级(低=绿/中=橙/高=红)
PriorityCard 是 3 档优先级选择(低/中/高),颜色对应风险色阶:
@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 })
}
关键三元 idx === 2 ? C.danger : idx === 1 ? C.warn : C.ok:
- 低(idx=0)→
C.ok绿色(#2BB673)——"风险低"用绿色 - 中(idx=1)→
C.warn橙色(#FF9F1C)——"中等风险"用橙色 - 高(idx=2)→
C.danger深红(#FF5A6E)——"高风险"用红色
绿-黄-红"风险色阶" 是国际通用的"严重性映射"(信号灯、地图、告警都这么用)。**"颜色 = 风险等级"**是直觉认知,无需文字解释。
注意 App 17 的 C.danger 是 #FF5A6E(偏深红),与主题色 #EF4444(偏鲜红)略有差异——"业务警示色与品牌色分离"是设计系统的成熟做法:品牌色是"主调",警示色是"风险信号",两套红但用途不同。"相同色系、不同饱和度/明度"是高级设计。
右上角实时显示当前优先级:Text(this.priorities[this.selectedPriority])(默认"中" idx=1)——@State selectedPriority 变化 → 文字同步。
六、SettingCard:Toggle 开关 + 隐私入口
SettingCard 是"提醒通知"开关(Toggle)+ "隐私设置"入口(› 箭头):
@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 风格开关isOn: this.remindOn— 受控属性(默认 true,默认开启提醒)selectedColor(C.primary)— 选中时主色(红)——**"主题色作为 Toggle 开启色"**是系列标准onChange((on: boolean) => { this.remindOn = on; })— 双向绑定
Divider() 分割线——上半部分(提醒通知)和下半部分(隐私设置)之间用 1vp 浅灰线分隔。Column({ space: 0 }) 因为分割线本身已经提供视觉间距,避免上下空白过大。
› 右箭头 — 表示"点击进入二级页面"(Text('›') 是常用 iOS 风格的"disclosure indicator")。"主开关 + 子设置"的两层结构避免一个开关页面塞 20 个 Toggle。
与 App 13 完全同源——包括 @State remindOn: boolean = true 默认值、selectedColor(C.primary) 主题色 Toggle、Toggle/Disclosure 双形态。
七、TemplateCard:3 个快捷模板
TemplateCard 是 3 个快捷模板(快速创建/高级模式/批量导入)的列表:
@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 })
}
3 个模板:
- 📋 快速创建 — 使用默认模板(懒人用户)
- 📋 高级模式 — 自定义所有字段(专家用户)
- 📋 批量导入 — 从文件导入(重度用户)
每行:📋 浅红底图标块(C.primarySoft)+ 主标题 + 副标题 + › 箭头。"图标块颜色跟随主题色"——C.primarySoft 是浅红(在红色主题下是品牌色的"软化版")。
与 App 13 完全同源——3 个模板、图标、主副标题、箭头全相同。"模板卡片"的视觉结构稳定复用。

八、SubmitBtn:动态文案 + 早返回校验
SubmitBtn 是表单的"终结点"——按钮文案根据标题是否填写动态变化:
@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)+ 白字(其实浅灰底 + 白字对比度不好,可能是设计小瑕疵)+ Toast 拦截 - 标题填写 → 文字"✓ 提交创建" + 主色背景(红)+ 白字
"白字"是写死的 fontColor('#FFFFFF')——在浅灰底上白字几乎看不见,这是设计 bug(#FFFFFF 在 C.cardSoft 上对比度极低)。真实项目应把"未填写"态的文字色改为 C.textDim 或 C.textSub。
早返回校验:
.onClick(() => {
if (this.inputTitle.length === 0) { promptAction.showToast({ message: '请输入标题' }); return; }
promptAction.showToast({ message: '创建成功!' });
this.inputTitle = ''; this.inputDesc = '';
})
if (!inputTitle) return 早返回 + Toast 提示——比 if-else 嵌套更扁平。成功路径除了弹 Toast 还清空两个 @State(inputTitle = '' + inputDesc = '')——用户提交后表单清空,可以立即再创建下一条。
Text('提交即表示同意相关条款') 法律提示——9sp 灰色居中,"小字+灰"层级最低但合规必须。任何"提交按钮"都该配一段条款提示(电商注册、隐私协议、金融产品都是)。

九、@State 的 5 状态映射
Func1Tab 有 5 个 @State:
| @State | 类型 | 用途 | 联动 |
|---|---|---|---|
inputTitle | string | 标题输入 | 按钮文案/按钮颜色/字数统计 |
inputDesc | string | 描述输入 | — |
selectedType | number | 类型选中(0-3) | TypeCard 高亮 |
selectedPriority | number | 优先级选中(0-2) | PriorityCard 高亮 + 文字显示 |
remindOn | boolean | 提醒通知开关 | Toggle 状态 |
inputTitle 是"最忙"的状态——同时驱动按钮文案、按钮颜色、字数统计 3 处 UI。一处状态、三处渲染是响应式 UI 的精髓。
selectedType + selectedPriority 是"单选"状态——典型模式 @State number + 索引。
remindOn 是"二元"状态——典型模式 @State boolean + Toggle 双向绑定。
5 个 @State = 5 个交互点——**"@State 数量 = 交互点数"**是设计健康状态管理的指标。

十、模板换肤的方法论(系列最重要的设计原则)
App 17 与 App 13 几乎 100% 同构——唯一的差异是 Theme.ets 里的几个颜色值:
| 颜色 | App 13(紫) | App 17(红) |
|---|---|---|
C.primary | #8B5CF6 | #EF4444 |
C.accent | #A78BFA | #F87171 |
C.primarySoft | #F0EBFE | #FEE8E8 |
C.warn / C.danger / C.ok | 同 | 同 |
"主题色 3 件套(主色/浅色/超浅色)换肤" + "业务色不变(C.warn/C.danger/C.ok 通用)" = 完整的模板换肤。
这套设计原则值得所有 ArkUI 项目学习:
- 定义"品牌色 3 件套"(
primary+accent+primarySoft)——所有品牌相关的 UI(按钮/选中态/进度条/链接)用这 3 个变量 - 定义"业务色"(
warn+danger+ok)——所有状态色用这 3 个变量(与品牌色解耦) - 所有组件用变量名而非色码——
.backgroundColor(C.primary)而不是.backgroundColor('#EF4444') - 换肤只改 Theme.ets——3 个颜色值全换 = 整个 App 换肤
这种"主题与代码分离"的模式让"换主题色"从"改 200 个文件"变成"改 1 个文件"——工程效率提升 200 倍。
真实项目建议:
- 暗色模式(夜间/省电):
dark: { primary: '#...', accent: '#...', ... }与light: { ... }配套 - 多品牌:企业版/个人版/校园版/医院版,换 Theme.ets 即可
- 节日主题:春节红、国庆红、圣诞绿,通过动态主题色实现(
AppStorage.set('primary', '#FF5A5A'))
"一套代码、多个主题"是 ArkUI 设计系统的最高目标——App 13/17 的对比就是这个目标的最佳示范。
十一、表单设计的"四层校验"进阶
App 17 SubmitBtn 只做了"标题非空"单校验,真实项目应做四层校验:
- 客户端校验(
@State同步校验)——本页当前实现 - 边界校验(如"标题不能全是空格"
if (this.inputTitle.trim().length === 0)) - 业务规则校验(如"高优先级活动必须填描述")
- 服务端校验(最强,最终防线)
"四层校验 = 客户端体验 + 业务完整性 + 数据安全"。demo 只做了第 1 层是简化,真实项目应把"边界校验"(trim/长度限制)也补上:
if (this.inputTitle.trim().length === 0) {
promptAction.showToast({ message: '标题不能为空' });
return;
}
if (this.inputTitle.length > 50) {
promptAction.showToast({ message: '标题最多 50 字' });
return;
}
trim() 是边界校验的关键——用户可能输入一堆空格" "绕过非空检查。trim().length 确保"实际有效内容"非空。
十二、Toggle 组件的"持久化"陷阱
@State remindOn: boolean = true 的"提醒通知"开关当前只存在内存里——应用重启后 remindOn 会重置为 true(默认值),用户设置的"关闭提醒"丢失。
真实项目应持久化到本地存储(@ohos.data.preferences):
import { preferences } from '@kit.ArkData';
aboutToAppear() {
// 读取持久化值
preferences.getPreferences(this.getUIContext().getHostContext(), 'volunteer_pref')
.then(p => p.get('remindOn', true))
.then(v => { this.remindOn = v; });
}
onChange((on: boolean) => {
this.remindOn = on;
// 持久化
preferences.getPreferences(this.getUIContext().getHostContext(), 'volunteer_pref')
.then(p => p.put('remindOn', on))
.then(p => p.flush());
})
aboutToAppear 是组件的生命周期钩子(类似 Vue 的 mounted),在组件创建时执行。getPreferences 异步读取 → 更新 @State → UI 重渲染。onChange 写持久化——**"状态变更即持久化"**是用户偏好设置的标准模式。
"Toggle 持久化"是 demo 简化——读者在自己项目里应记住:用户偏好必须持久化,否则重启丢失。
十三、总结
App 17 活动创建页解析完毕。与 App 13 的"模板换肤"关系展示了一个重要方法论:一套 UI 框架 + 一套 Theme.ets 配色 = 任意主题的应用。"红色志愿"主题色让 6 个表单组件焕发公益热情——C.primary 红色在按钮/选中态/进度条/链接文字上反复出现,让"志愿"主题深入骨髓。
"表单校验"和"Toggle 持久化"是 demo 到产品的关键差距——读者在抄这个模板时记得补齐。
更多推荐





所有评论(0)