HarmonyOS 校园应用系列之ArkTS 表单工程:打印文件创建页的受控组件与分组卡片实践
一、页面定位与功能概述
文件创建页(Func1Tab)是校园打印 App 的"数据录入中心"——用户在此创建打印任务,填写标题、描述、选择打印类型和优先级。页面标题为"创建",副标题"填写信息快速创建"明确了单一职责。
本页是纯粹的"数据录入型页面"——包含 TextInput、TextArea、Toggle 等多种受控表单组件,是学习 ArkUI 表单开发的最佳实战案例。页面采用"Section 分组卡片"布局,每个功能区用白色圆角卡片包裹,卡片间距 14vp(Column({ space: 14 }))。
1.1 页面状态定义
@State inputTitle: string = '' // 文档标题 @State inputDesc: string = '' // 详细描述 @State selectedType: number = 0 // 选中的类型索引 @State selectedPriority: number = 1 // 优先级:0=低, 1=中, 2=高 @State remindOn: boolean = true // 提醒通知开关5 个@State变量覆盖所有输入维度。selectedType和selectedPriority使用 number 类型作为数组索引,比 string 类型更语义化。
项目源码开源:https://gitee.com/codenestFlow/HarmonyOSHub
二、页面头部 —— 标题 + 操作按钮
@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 }) }头部是"主标题+副标题"双行结构:左侧 20vp 加粗"创建"与 10vp 灰色副标题"填写信息快速创建"纵向排列,明确页面单一职责。右侧是 📋 emoji 图标放在C.cardSoft底、D.rSm圆角的 36×36 方块中,暗示可从剪贴板粘贴文档信息,这是"降低输入成本"的典型策略。头部高度safeTop + 60配合alignItems(VerticalAlign.Bottom)让内容沉底对齐,底部 1vpC.stroke分隔线与白底C.card构成通栏吸顶效果。
三、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 vs TextArea 对比
| 特性 | TextInput | TextArea |
|---|---|---|
| 适用场景 | 单行短文本(标题) | 多行长文本(描述) |
| 默认行为 | Enter 不换行 | Enter 换行 |
| 高度 | 固定 44vp | 固定 90vp |
两者共享相同样式体系(D.rSm圆角 10vp、C.cardSoft浅灰背景),保持视觉一致性。
3.2 受控组件双向绑定模式
TextInput({ placeholder: '请输入标题', text: this.inputTitle }) // ① 外部传入当前值 .onChange((v: string) => { // ② 用户输入时回调 this.inputTitle = v // ③ 更新状态变量触发重渲染 })这是 ArkUI 标准的受控组件模式—— UI 显示完全由状态决定,不存在"显示值和存储值不一致"的问题。在表单校验、重置、回填场景中尤为关键。
3.3 实时字数统计
${this.inputTitle.length}/50展示标题输入框的实时字数——即时反馈让用户每输入一个字符就看到变化,分母 50 隐式传达上限。接近或超限时可将数字变为橙色/红色警告。
3.4 附件按钮设计
图片和链接按钮使用"emoji 图标+文字+色块"组合(📷 图片、🔗 链接,浅灰背景C.cardSoft),像可点击的标签(Tag Button),比纯文字或纯图标更醒目。
四、TypeCard —— 图标化四选一类型选择器
@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 }) }4.1 选中态的四重同步变化
选中某类型时同时发生4 个视觉属性变化:
| 属性 | 未选中 | 选中 |
|---|---|---|
| 图标背景色 | 浅灰C.cardSoft | 浅绿C.primarySoft(色块强调) |
| 图标边框 | 0vp | 2vp 绿色描边 |
| 文字颜色 | 灰C.textDim | 绿C.primary(文字高亮) |
| 文字字重 | Normal | Bold(加粗强化) |
图标是 emojiText(📝 类型一 / 🎯 类型二 / ⭐ 类型三 / 📊 类型四),放在 48×48、D.rMd圆角的方块中。多重信号叠加的效果远超单一属性切换——即使不看文字,仅凭色块+边框就能识别选中项。
4.2 为什么不用 Radio 组件?
标准 Radio 只能显示圆形选择器,无法展示业务图标;需要横向一行四列排列而非垂直列表;选中态需要使用主题绿色。当标准组件无法满足需求时,用基础组件组合自定义是合理的选择。
五、PriorityCard —— 三档优先级
@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 }) }5.1 当前值回显
卡片标题行右侧用Text(this.priorities[this.selectedPriority])实时回显当前选中的优先级(绿色加粗),用户无需扫视下方按钮即可确认当前状态。
5.2 交通灯色彩语义
| 优先级 | 色值 | 含义 |
|---|---|---|
| 低 | C.ok(#2BB673)绿色 | 冷静、不紧急 |
| 中 | C.warn(#FF9F1C)琥珀 | 注意、默认推荐 |
| 高 | C.danger(#FF5A6E)红色 | 紧急、立即关注 |
选中项填充对应语义色 + 白字,未选中项统一为C.cardSoft浅灰底 +C.textSub灰字。默认值selectedPriority = 1即"中"档,大多数用户会接受默认选项以减少决策成本。
六、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组件(ToggleType.Switch滑动开关),isOn绑定remindOn状态,.selectedColor(C.primary)让开启态轨道为主题绿。图标是 emojiText('🔔')放在 32×32 浅灰方块中。卡片下半部分是"隐私设置"跳转项(🔒 图标 +›箭头),中间用Divider().color(C.stroke)分隔——开关项与跳转项合并在同一张卡片是设置页的经典分组手法。
HarmonyOS 提供三种 Toggle 类型:Switch(iOS 风格滑动开关)、Checkbox(复选框)、Button(独立切换按钮)。开/关二态用 Toggle 更直观;多选用 Checkbox 更语义准确。"提醒通知"明显是二态开关。
七、TemplateCard 与 SubmitBtn —— 快捷模板与提交按钮
@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 }) } @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。提交按钮是状态自适应按钮:标题为空时显示"请填写标题"(C.cardSoft灰底),有输入时变为"✓ 提交创建"(C.primary绿底)——按钮文案与背景色随inputTitle.length联动,无需额外校验提示。点击时若标题为空弹 toast "请输入标题"并 return;成功则弹"创建成功!"并清空inputTitle、inputDesc两个状态完成表单重置。按钮下方 9vp 灰字"提交即表示同意相关条款"是合规提示。
八、表单设计六大最佳实践
- 视觉分组:每个功能区用独立白色圆角卡片包裹,降低认知负荷
- 渐进式 disclosure:核心信息 → 分类 → 优先级 → 辅助设置 → 模板 → 提交,符合自然思维流程
- 即时反馈:字数实时更新、选中态即时切换、Toggle 即时响应、按钮文案随输入变化
- 合理默认值:类型默认第一项、优先级默认"中"、提醒默认开启
- 清晰标签体系:输入字段上方小号灰色标签 + placeholder 补充上下文
- 操作引导:快捷模板降低输入成本、提交按钮文案引导补全标题
九、表单校验与提交流程
源码中的提交校验逻辑非常轻量——只校验标题:
.onClick(() => { if (this.inputTitle.length === 0) { promptAction.showToast({ message: '请输入标题' }); return; } promptAction.showToast({ message: '创建成功!' }); this.inputTitle = ''; this.inputDesc = ''; })若要扩展为完整的生产级流程,可在此基础上增加四步:
// 第一步:前端校验 const canSubmit = this.inputTitle.trim().length > 0 && this.inputDesc.trim().length > 0; if (!canSubmit) { showToast('请填写完整信息'); return; } // 第二步:数据组装 const payload = { title: this.inputTitle, content: this.inputDesc, type: this.types[this.selectedType].name, priority: ['低','中','高'][this.selectedPriority], reminder: this.remindOn }; // 第三步:异步请求(http 模块 POST) // 第四步:状态重置或路由跳转十、键盘适配方案
真机上键盘弹出可能遮挡输入框。三种解决方案:
.expandSafeArea([SafeAreaType.KEYBOARD])— 自动避让- Scroll 包裹输入区域(推荐,本页已采用)— 最简单,框架自动处理
- 监听
window.on('keyboardHeightChange')动态调整 padding
本页主内容区已经包裹在Scroll中(.layoutWeight(1).scrollBar(BarState.Off).align(Alignment.Top)),键盘弹出时用户可滚动定位被遮挡的输入框。
十一、总结
文件创建页展示了 ArkUI 表单开发的完整模式:Section 卡片分组布局保持视觉清晰,受控组件双向绑定确保数据一致性,图标化选择器提供超越标准组件的表现力,状态自适应提交按钮引导用户补全关键信息。这些模式可直接复用到任何需要数据录入的场景。