鸿蒙原生应用 ArkTS 表单工程:书库创建页的类型选择与动态提交按钮
鸿蒙原生应用 ArkTS 表单工程:书库创建页的类型选择与动态提交按钮
App 20「阅读管理」书库 Tab(Func1Tab),是添加书籍的表单页。整页由 6 个 @Builder 块拼出 Header + 基本信息 + 类型选择 + 优先级选择 + 设置 + 快捷模板 + 提交按钮(动态文案)。与 App 13/17 创建表单几乎完全同源——是"模板换肤适配新业务"的又一例证。本篇基于
20-reading-manager/entry/src/main/ets/pages/Func1Tab.ets(约 215 行)逐段拆解,附 4 张实机截图。
一、整体结构:6 个 @Builder 的表单漏斗
Func1Tab 的骨架与 App 13/17 创建页完全同源(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 + FormCard:标题/描述/字数统计
Header 是"创建/填写信息快速创建"单行 + 右侧 📋 图标(与 App 13 同构)。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 + 字数统计三件套(与 App 13/17 完全同源):
TextInput44vp 单行 +TextArea90vp 多行(3-4 行)onChange双向绑定(text: this.inputTitle受控 + 回调写回)${this.inputTitle.length}/50字数统计——输入实时更新"0/50→1/50→2/50"
"50 字上限"的语义(在阅读 App 里)——书名标题要精简("三体"两个字),50 字足够任何书名的长度——"上限 = 业务约束"(App 13 音乐 50 字、App 20 书名 50 字同款)。
"📷 图片 / 🔗 链接"伪按钮——添加书的封面图/豆瓣链接(阅读 App 添加书常要填封面和链接)——Blank() 撑开把字数统计推到右侧。
三、TypeCard:4 种类型图标网格
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; }) 更新。
"类型一/二/三/四"的通用命名(App 13/17 同款)——"类型名没换"是模板换肤的遗留(真实阅读 App 应换成"文学/科幻/历史/心理"等真实分类)——"通用命名 vs 业务命名"的取舍(demo 用通用名省事,读者做产品应换业务名)。
四、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:
- 低 → 绿(
C.ok)、中 → 橙(C.warn)、高 → 红(C.danger)
"绿-黄-红"国际通用风险色阶(信号灯语义)。优先级在阅读 App 里的语义——"这本书多急着读"(高优先级 = 马上要读,如课程指定书)。
右上角实时显示 priorities[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.SwitchiOS 风格开关isOn: this.remindOn(默认 true,默认开启提醒)selectedColor(C.primary)紫色开启色onChange双向绑定
"提醒通知"在阅读 App 里的语义——"阅读打卡提醒"(每天 20:00 提醒读书)——"提醒 = 习惯养成"(阅读 App 的核心价值是帮用户养成阅读习惯)。
Divider() 分割线 + Column({ space: 0 })(分割线本身提供间距)。› 箭头表示二级页面(隐私设置入口)。
六、TemplateCard + SubmitBtn
TemplateCard 是 3 个快捷模板(快速创建/高级模式/批量导入)列表(与 App 13/17 同源):
@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 })
}
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%')
}
两态按钮:
- 标题为空 → "请填写标题" + 浅灰底 + Toast 拦截
- 标题填写 → "✓ 提交创建" + 紫底白字 + Toast "创建成功!" + 清空两个输入
早返回校验(if (!title) return)——比 if-else 嵌套更扁平。成功清空(inputTitle = ''; inputDesc = '')——提交后可立即再添加。
注意:"提交即表示同意相关条款"在"添加书"场景有点违和(看书 App 加书不需要同意条款)——"模板复刻的文案遗留"(App 13 的音乐创建表单文案没换)——**"业务文案要随业务换"**是模板复刻必须注意的点(真实阅读 App 应改成"添加后将同步到书架"之类)。
七、@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 处)——"一处状态、三处渲染"。
5 个 @State = 5 个交互点——"@State 数量 = 交互点数"规律(App 13/17/20 表单页都是 5 个,高度稳定)。
八、"创建表单"模板的系列复用史
App 20 书库页与 App 13 歌曲创建、App 17 活动创建几乎 100% 同源——回顾系列 3 个"创建表单":
| App | 主题 | 业务 | @State | 提交校验 |
|---|---|---|---|---|
| 13 音乐 | 紫 #8B5CF6 | 添加歌曲 | 5 | 标题非空 |
| 17 志愿 | 红 #EF4444 | 创建活动 | 5 | 标题非空 |
| 20 阅读 | 紫 #A855F7 | 添加书籍 | 5 | 标题非空 |
3 个创建表单的差异只有"主题色 + 图标 + 文案"——"创建表单模板"已稳定复用 3 次——"FormCard/TypeCard/PriorityCard/SettingCard/TemplateCard/SubmitBtn 六件套"是 ArkUI 创建类页面的标准骨架。
"创建表单六件套"可复用到:添加歌曲、创建活动、添加书籍、新增订单、发布帖子、新建任务——任何"填信息 + 选类型 + 设优先级 + 开关设置 + 模板 + 提交"的场景。

九、表单校验的"边界防护"进阶
App 20 SubmitBtn 只做了"标题非空"单校验(if (this.inputTitle.length === 0)),真实项目应加"边界防护":
1. 空串校验(当前有):inputTitle.length === 0 → 拦截
2. 纯空格校验(应补):
if (this.inputTitle.trim().length === 0) {
promptAction.showToast({ message: '标题不能为空白' });
return;
}
trim() 去掉首尾空格——用户输入" "(纯空格)时 .length 是 3 但 .trim().length 是 0——**"trim 校验防纯空格"**是表单必备。
3. 长度上限(应补):
if (this.inputTitle.length > 50) {
promptAction.showToast({ message: '标题最多 50 字' });
return;
}
4. 字数统计联动:${this.inputTitle.length}/50 达到 50 时输入框变红(C.danger)提示——**"视觉预警"**比"事后拦截"更友好。
"边界防护 = 空串 + 纯空格 + 长度 + 格式"四件套——demo 只做了空串,真实项目要补全(尤其是 trim 防空格——"空格绕过非空校验"是表单最常见的漏洞)。
十、"六件套"表单模板的组件化建议
App 20 书库页的"表单六件套"(FormCard/TypeCard/PriorityCard/SettingCard/TemplateCard/SubmitBtn)是高度复用的模板,建议拆成独立组件:
方案一:@Builder 参数化(当前做法)——适合"单页使用",6 个 @Builder 在 Func1Tab 内。
方案二:独立 @Component(推荐真实项目)——把 FormCard/TypeCard 抽成 components/ 下的独立组件:
@Component
export struct TypeSelector {
@Prop selectedType: number;
@Prop options: OptionItem[];
onSelect: (idx: number) => void; // 回调
...
}
"抽组件"的 3 个好处:
- 跨页复用——书籍创建页、笔记创建页、书签创建页都能用 TypeSelector
- 独立测试——每个表单组件可单独 Preview 调试
- 协作分工——A 写 FormCard、B 写 TypeCard,互不干扰
"六件套"抽组件后,任何"创建类页面"只需拼装:
build() {
Column() {
FormCard(...)
TypeSelector(...)
PrioritySelector(...)
SettingToggle(...)
TemplatePicker(...)
SubmitButton(...)
}
}
"表单组件化"是 ArkUI 工程化的重要一步——App 20 的六件套是现成的拆分清单。

十一、"模板复刻"的正确姿势
App 20 书库页与 App 13/17 创建页的同源,展示了一个关键方法论——"模板复刻"的正确姿势:
该复刻的(代码结构):
- 6 个 @Builder 的布局骨架
- @State 状态管理(5 个)
- TextInput/TextArea/Toggle/ForEach 交互
- 主题色变量引用(C.primary/C.cardSoft)
该换的(业务内容):
- 占位文案("请输入标题" → "请输入书名")
- 类型名("类型一/二" → "文学/科幻/历史")
- 提交成功文案("创建成功" → "已添加到书架")
- 条款文案("同意相关条款" → "添加后将同步书架")
**"复刻骨架、换业务"**是模板复刻的正确打开方式——App 13/17/20 的书库页复刻了骨架但没换业务文案("类型一"仍是通用名)——读者应把"该换的"清单记下来:占位/类型/成功文案/条款/数据全换。

十二、阅读场景下的"表单语义"再设计
App 20 书库页虽是模板复刻,但**"添加书"场景下的表单语义**值得重新设计(读者可对比 App 13 音乐场景理解"同一表单不同语义"):
FormCard(基本信息):
- 音乐场景:标题 = 歌曲名、描述 = 歌曲介绍
- 阅读场景:标题 = 书名、描述 = 书籍简介——"输入框语义随业务"(占位符应改"请输入书名")
TypeCard(类型):
- 音乐场景:类型一/二/三/四(流派占位)
- 阅读场景:应改为"文学/科幻/历史/心理学"(真实分类)——"类型选项 = 业务分类体系"
PriorityCard(优先级):
- 音乐场景:歌曲重要程度
- 阅读场景:"这本书多急着读"(如"高" = 课程指定必读)——"优先级语义可复用"(急迫程度通用)
SettingCard(设置):
- 音乐场景:提醒通知(新歌提醒)
- 阅读场景:阅读提醒(每天 20:00 提醒读书)——"提醒 = 习惯养成"——阅读 App 的提醒比音乐更有价值(读书需要自律,提醒帮用户坚持)
TemplateCard(模板):
- 音乐场景:快速创建/高级模式/批量导入
- 阅读场景:"从豆瓣导入""ISBN 扫码添加""书单模板"——"模板 = 业务快捷方式"
SubmitBtn(提交):
- 音乐场景:创建成功
- 阅读场景:"已添加到书架"——"成功反馈 = 业务动作确认"
"同一表单六件套,业务语义全换"——这是"表单模板复用"的最高级姿势:骨架不变、语义全换,就是完全不同的产品。
十三、总结
App 20 书库创建页解析完毕。与 App 13/17 的"模板复刻"展示了创建表单模板的稳定复用——"表单六件套"是 ArkUI 创建类页面的标准骨架。同时指出"类型名/条款文案"等模板复刻的遗留问题(读者应随业务替换)。

更多推荐




所有评论(0)