HarmonyOS 应用 · 校园宿舍拼单拼单发起页深度解析:一个表单驱动的 ArkUI 实战

发布时间:2026/8/24 13:31:48
HarmonyOS 应用 · 校园宿舍拼单拼单发起页深度解析:一个表单驱动的 ArkUI 实战 HarmonyOS 应用 · 校园宿舍拼单拼单发起页深度解析一个表单驱动的 ArkUI 实战本文以 HarmonyOS 6.0API 23 ArkTS 声明式 UI 为技术底座逐行拆解「校园宿舍拼单」应用拼单TabFunc1Tab的搭建思路——从模板选择到表单填写从状态管理到发布校验。所有截图均来自本人电脑正在运行的 HarmonyOS 模拟器真机画面。一、项目缘起为什么需要发起拼单首页是逛的入口而拼单页是建的入口。一款完整的拼单工具必须让用户能在 1 分钟内完成标题 → 分类 → 人数 → 截止 → 选项 → 发布的全部流程。如果表单设计得繁琐、选项组织得混乱用户会在第一次使用时放弃。「校园宿舍拼单」的Func1Tab把整个流程拆成 8 张语义卡片每张卡片承载一个明确的决策维度。这种卡片化 强校验的模式既符合用户在 App 里的阅读节奏一次只看一件事又便于后续接入真实接口每张卡片对应一个独立的字段组。整个Func1Tab文件位于entry/src/main/ets/pages/Func1Tab.ets约 300 行 ArkTS 代码由 8 个Builder卡片 1 个SubmitBtn 1 个Header组成。下面我们从架构到细节逐节拆解。项目源码开源https://gitee.com/codenestFlow/HarmonyOSHub二、整体架构Func1Tab 在工程中的位置与首页一样Func1Tab也是Index.ets的 4 个TabContent之一Tabs({ barPosition: BarPosition.End, index: this.current }) { TabContent() { HomeTab() }.tabBar(this.bar(首页, , 0)) TabContent() { Func1Tab() }.tabBar(this.bar(拼单, , 1)) TabContent() { Func2Tab() }.tabBar(this.bar(订单, , 2)) TabContent() { ProfileTab() }.tabBar(this.bar(我的, , 3)) }Func1Tab的build()与首页结构相似外层Column装一个固定Header下面是占满剩余高度的Scroll内部Column({ space: 16 })依次堆叠 8 张卡片 1 个发布按钮。build() { Column() { this.Header() Scroll() { Column({ space: 16 }) { this.TemplateCard() this.FieldCard() this.CategoryCard() this.PeopleCard() this.DeadlineCard() this.OptionCard() this.PreviewCard() this.SubmitBtn() } .width(100%) .padding({ left: D.pad, right: D.pad, top: 16, bottom: D.pad this.safeBottom 20 }) } .layoutWeight(1).scrollBar(BarState.Off).align(Alignment.Top) } .width(100%).height(100%).backgroundColor(C.bg) }这里有几个关键差异space: 16比首页的space: 14稍大因为表单卡片内部有多个输入项间距过密会让用户觉得挤。滚动到底部 safeBottom 20确保发布按钮不会贴在 Tab 栏下面。safeTop与safeBottom仍由AppStorage提供与首页共用同一份全局避让数据。下面这张图是 HarmonyOS 模拟器里拼单页首屏的真实运行效果可以看到顶部 Header 是发起拼单下面依次是快捷模板横滚卡奶茶拼单 / 炸鸡拼单 / 麻辣烫拼单、拼单信息表单卡标题输入框 备注 单价/店铺 商品图片占位、选择分类卡饮料 / 零食 / 外卖 / 日用。三、状态地图12 个State字段Func1Tab是整个项目里状态最密集的页面——一个完整的表单对应 12 个State字段State category: string 饮料; State people: number 5; State deadline: string 今晚 22:00; State title: string ; State price: string ; State shop: string ; State remark: string ; State delivery: string 送到宿舍; State payType: string AA; State needImage: boolean false; State autoConfirm: boolean true; State showPreview: boolean false;按业务维度分组核心信息title拼单标题、price单价、shop店铺、remark备注分类与规模category分类、people人数、deadline截止时间配送与支付delivery配送方式、payType支付方式、autoConfirm自动确认收货辅助状态needImage是否添加商品图、showPreview实时预览是否展开这 12 个状态在 ArkTS 的响应式系统下形成了一个典型的表单状态机每个输入控件如TextInput、Toggle通过onChange回调更新状态状态变化触发依赖它的视图组件重渲染。整个页面没有任何手动 DOM 操作这就是声明式 UI 的力量。四、Header极简风格Builder Header() { Row() { Text(发起拼单).fontSize(20).fontWeight(FontWeight.Bold).fontColor(C.text) } .width(100%).height(this.safeTop 56) .padding({ top: this.safeTop, left: D.pad, right: D.pad }) .backgroundColor(C.card).alignItems(VerticalAlign.Bottom) }与首页的主标题 副标题不同Func1Tab的 Header 只有一行发起拼单字号 20、加粗、纯白底。高度safeTop 56让标题文字刚好落在状态栏下方 28vp 处56 的一半。之所以这么素是因为页面下半部分的卡片本身已经信息密度很高Header 再加副标题会显得喧宾夺主。这种重内容、轻标题的处理方式在表单页里非常常见。五、TemplateCard快捷模板一键套用Builder TemplateCard() { Column({ space: 12 }) { Text(快捷模板).fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text) Scroll() { Row({ space: 10 }) { ForEach(this.templates, (t: Template) { Column({ space: 4 }) { Row() { Text(t.emoji).fontSize(28) } .width(48).height(48).backgroundColor(C.primarySoft).borderRadius(D.rSm) .justifyContent(FlexAlign.Center) Text(t.name).fontSize(12).fontColor(C.text) Text(t.desc).fontSize(9).fontColor(C.textDim).maxLines(1) } .width(90).padding({ top: 10, bottom: 10 }) .backgroundColor(C.cardSoft).borderRadius(D.rMd) .border({ width: 1, color: C.stroke }) .onClick(() { this.title t.name; this.category this.cats[0]; promptAction.showToast({ message: 已套用: t.name }); }) }, (t: Template) t.id.toString()) } } .scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off).width(100%) } .width(100%).padding(14).backgroundColor(C.card) .borderRadius(D.rLg).border({ width: 1, color: C.stroke }) }TemplateCard的设计意图很明确让懒得填的用户也能一键发起拼单。4 个预设模板private templates: Template[] [ { id: 1, emoji: , name: 奶茶拼单, desc: 第二杯半价5人起拼 }, { id: 2, emoji: , name: 炸鸡拼单, desc: 全家桶团购价更划算 }, { id: 3, emoji: , name: 麻辣烫拼单, desc: 满减优惠一起点 }, { id: 4, emoji: , name: 饮料囤货, desc: 整箱购买更便宜 } ];每个模板是一张 90vp 宽的小卡片顶部是 emoji 头像48×48vp主色背景中间是模板名12 号字底部是简短描述9 号字、单行省略。点击模板会触发两件事this.title t.name把模板名自动填到拼单标题输入框。this.category this.cats[0]把分类默认设为饮料第一个分类。这种点击模板就填充表单的设计能把首次发起的填表时间从 1 分钟缩短到 10 秒。从我自己的体验看快捷模板是这种工具型 App非常关键的设计——它降低了用户首次使用的心智成本。注意ForEach第三参数用了t.id.toString()作为 keyGenerator这是推荐做法有助于提升列表渲染性能和组件复用准确性。六、FieldCard表单的核心实时校验Builder FieldCard() { Column({ space: 12 }) { Text(拼单信息).fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text) TextInput({ placeholder: 拼单标题如奶茶第二杯半价, text: this.title }) .onChange((v: string) { this.title v; }) .height(44).backgroundColor(C.cardSoft).borderRadius(D.rSm) .placeholderColor(C.textDim).placeholderFont({ size: 14 }) if (this.title.length 0) { Text(⚠ 标题不能为空).fontSize(11).fontColor(C.warn) } TextArea({ placeholder: 备注口味、数量、配送地址等, text: this.remark }) .onChange((v: string) { this.remark v; }) .height(80).backgroundColor(C.cardSoft).borderRadius(D.rSm) .placeholderColor(C.textDim).placeholderFont({ size: 14 }) Row({ space: 10 }) { TextInput({ placeholder: 单价 ¥, text: this.price }) .onChange((v: string) { this.price v; }) .height(44).backgroundColor(C.cardSoft).borderRadius(D.rSm) .placeholderColor(C.textDim).placeholderFont({ size: 14 }).layoutWeight(1) TextInput({ placeholder: 店铺/商家, text: this.shop }) .onChange((v: string) { this.shop v; }) .height(44).backgroundColor(C.cardSoft).borderRadius(D.rSm) .placeholderColor(C.textDim).placeholderFont({ size: 14 }).layoutWeight(1) }.width(100%) Row({ space: 10 }) { Column({ space: 6 }) { Text(商品图片).fontSize(13).fontColor(C.textSub) Row() { Text().fontSize(28) } .width(64).height(64).backgroundColor(C.cardSoft).borderRadius(D.rSm) .border({ width: 1, color: C.stroke, style: BorderStyle.Dashed }) .justifyContent(FlexAlign.Center) .onClick(() { this.needImage !this.needImage; promptAction.showToast({ message: this.needImage ? 已添加图片 : 已移除图片 }); }) }.alignItems(HorizontalAlign.Start) Blank() }.width(100%) } .width(100%).padding(14).backgroundColor(C.card) .borderRadius(D.rLg).border({ width: 1, color: C.stroke }) }这张卡片是整个表单的核心包含 5 类输入控件 1 个条件渲染的警告提示。6.1 拼单标题必填校验if (this.title.length 0) { Text(⚠ 标题不能为空).fontSize(11).fontColor(C.warn) }这是一个非常优雅的实时校验实现标题为空时下方自动出现一行橙色警告文字⚠ 标题不能为空用户开始输入警告文字立即消失。不需要点击提交才知道错在哪——这种所见即所得的校验让用户在填表过程中就能感受到表单的约束。Text内的 emoji “⚠” 用一个字符代替了矢量图标省资源且跨平台一致。6.2 备注多行文本TextArea高度 80vp容纳约 3 行placeholder 提示口味、数量、配送地址等引导用户写得更完整。这与首页的立即拼按钮只展示单行信息形成鲜明对比——表单页鼓励详细。6.3 单价 店铺等宽两栏两个TextInput用Row({ space: 10 })横向并排每个layoutWeight(1)自然等分剩余宽度。这种双输入项并排是移动端表单的经典模式能在横向空间允许的情况下提升信息密度。6.4 商品图片虚线占位border({ width: 1, color: C.stroke, style: BorderStyle.Dashed })用虚线边框区分占位与已上传。点击占位区域会切换this.needImage状态并通过 toast 反馈已添加图片 / 已移除图片。这是一个非常聪明的无图占位实现——它不需要真实的图片上传逻辑但能让用户感受到图片可以加的产品能力。七、CategoryCard4 个分类 ChipBuilder CategoryCard() { Column({ space: 12 }) { Text(选择分类).fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text) Row({ space: 10 }) { ForEach(this.cats, (cat: string) { Text(cat).fontSize(13) .fontColor(this.category cat ? #FFFFFF : C.textSub) .padding({ left: 16, right: 16, top: 8, bottom: 8 }) .backgroundColor(this.category cat ? C.primary : C.cardSoft) .borderRadius(16) .onClick(() { this.category cat; }) }, (cat: string) cat) }.width(100%) } .width(100%).padding(14).backgroundColor(C.card) .borderRadius(D.rLg).border({ width: 1, color: C.stroke }) }cats是 4 个分类的简单数组[饮料, 零食, 外卖, 日用]。每个分类是一颗 Chip未选中时浅色背景 次文色选中时主色背景 白字。点击切换this.category4 个 Chip 中只有一个能保持选中态互斥单选。这种互斥单选 Chip 群是 ArkUI 的基础模式但实现得很干净——Row({ space: 10 })让 Chip 之间留出 10vp 间距.borderRadius(16)让 Chip 呈胶囊形整行有糖果般的活泼感。与首页ChipRow的横向滚动不同这里的 Chip 数量只有 4 个且每颗字数较少不需要横向滚动4 颗一次性放下足够。这种按需设计的克制是 ArkUI 的核心理念——不为可能的需求过度设计。八、PeopleCard6 颗圆形数字按钮Builder PeopleCard() { Column({ space: 12 }) { Row() { Text(拼单人数).fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text) Blank() Text(this.people.toString() 人).fontSize(15).fontColor(C.primary).fontWeight(FontWeight.Bold) }.width(100%) Row({ space: 12 }) { ForEach([2, 3, 4, 5, 6, 8], (n: number) { Text(n.toString()).fontSize(14) .fontColor(this.people n ? #FFFFFF : C.textSub) .width(40).height(40).textAlign(TextAlign.Center) .backgroundColor(this.people n ? C.primary : C.cardSoft) .borderRadius(20) .onClick(() { this.people n; }) }, (n: number) n.toString()) }.width(100%) } .width(100%).padding(14).backgroundColor(C.card) .borderRadius(D.rLg).border({ width: 1, color: C.stroke }) }人数选择用 6 颗圆形按钮[2, 3, 4, 5, 6, 8]。注意数组里跳过了 7——这是产品设计的小心思因为 7 人拼单在宿舍场景里非常罕见跳过能减少选择噪音经典希克定律选项越少决策越快。右上角动态显示当前选中值5 人主色加粗。这种标题旁边实时显示当前值是表单交互的常见模式——用户一眼就能看到现在选的是什么无需扫视整行按钮。每颗按钮 40×40vp 圆角 20vp完全圆形未选中浅色背景次文色选中主色背景白字。这种尺寸 颜色双重反馈在表单页里非常常见能让按下 / 松开的状态感非常清晰。九、DeadlineCard4 颗时间 ChipBuilder DeadlineCard() { Column({ space: 12 }) { Text(截止时间).fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text) Row({ space: 10 }) { ForEach(this.deadlines, (d: string) { Text(d).fontSize(13) .fontColor(this.deadline d ? C.primary : C.textSub) .padding({ left: 12, right: 12, top: 8, bottom: 8 }) .backgroundColor(this.deadline d ? C.primarySoft : C.cardSoft) .borderRadius(16) .onClick(() { this.deadline d; }) }, (d: string) d) }.width(100%) } .width(100%).padding(14).backgroundColor(C.card) .borderRadius(D.rLg).border({ width: 1, color: C.stroke }) }截止时间提供了 4 个候选[今晚 20:00, 今晚 22:00, 明天 12:00, 明天 18:00]。与 CategoryCard 不同的是选中态用primarySoft浅橙背景 primary主色文字而不是主色背景 白字——这是一种更柔和的轻量选中视觉处理。原因可能是时间选择比分类选择权重更低用户每天都会改时间但很少改分类。用更轻的视觉反馈能避免页面太吵。这种按场景选视觉强度是设计中的重要原则。下图是拼单页中段可以看到选择分类、拼单人数、截止时间、更多选项的视觉效果可以看到右上角实时显示当前选中的人数5 人5 这个数字按钮是橙色高亮“今晚 22:00” 是浅橙背景主色文字轻量选中下方更多选项卡片的送到宿舍“AA” 是主色背景白字强选中。同一页面里两种选中态强度的并存正好印证了按权重选视觉的设计哲学。十、OptionCard配送方式 支付方式 自动确认Builder OptionCard() { Column({ space: 14 }) { Text(更多选项).fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text) Row() { Text(配送方式).fontSize(14).fontColor(C.textSub) Blank() Text(this.delivery).fontSize(14).fontColor(C.primary) }.width(100%) Row({ space: 8 }) { ForEach(this.deliveries, (d: string) { Text(d).fontSize(12) .fontColor(this.delivery d ? #FFFFFF : C.textSub) .padding({ left: 10, right: 10, top: 6, bottom: 6 }) .backgroundColor(this.delivery d ? C.primary : C.cardSoft) .borderRadius(12) .onClick(() { this.delivery d; }) }, (d: string) d) }.width(100%) Divider().color(C.stroke) Row() { Text(支付方式).fontSize(14).fontColor(C.textSub) Blank() Text(this.payType).fontSize(14).fontColor(C.primary) }.width(100%) Row({ space: 8 }) { ForEach(this.payTypes, (p: string) { Text(p).fontSize(12) .fontColor(this.payType p ? #FFFFFF : C.textSub) .padding({ left: 10, right: 10, top: 6, bottom: 6 }) .backgroundColor(this.payType p ? C.primary : C.cardSoft) .borderRadius(12) .onClick(() { this.payType p; }) }, (p: string) p) }.width(100%) Divider().color(C.stroke) Row() { Text(自动确认收货).fontSize(14).fontColor(C.textSub) Blank() Toggle({ type: ToggleType.Switch, isOn: this.autoConfirm }) .selectedColor(C.primary) .onChange((on: boolean) { this.autoConfirm on; }) }.width(100%) } .width(100%).padding(14).backgroundColor(C.card) .borderRadius(D.rLg).border({ width: 1, color: C.stroke }) }OptionCard是整个表单里最复杂的一张卡——它用Divider()把内容切成 3 个区块配送方式 → 支付方式 → 自动确认收货。每个区块都是标题 当前值 一行选择器的统一结构。10.1 配送方式3 选 1候选[送到宿舍, 楼下自取, 校门口取]。校园场景下校门口取是性价比最高的方式省配送费但学生常在宿舍又想送到宿舍。这种贴合真实场景的选项设计是校园工具型 App 的重要细节。10.2 支付方式3 选 1候选[AA, 团长付, 均摊]。AA 是默认选项也是宿舍最常见的方式团长付适合关系好的小团体均摊则是模糊场景下的折中方案。10.3 自动确认收货Toggle 开关Toggle({ type: ToggleType.Switch, isOn: this.autoConfirm }) .selectedColor(C.primary) .onChange((on: boolean) { this.autoConfirm on; })Toggle是 ArkUI 的开关控件ToggleType.Switch是 iOS 风格的圆角开关。selectedColor(C.primary)让开关选中态使用主色与整个 App 的橙色主题统一。onChange回调接收布尔值同步更新this.autoConfirm状态。默认autoConfirm true开启后系统在订单送达时自动确认收货省去手动确认的步骤——这是减负型设计。十一、PreviewCard实时预览所见即所得Builder PreviewCard() { Column({ space: 10 }) { Row() { Text(实时预览).fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text) Blank() Text(this.showPreview ? 收起 ▲ : 展开 ▼).fontSize(12).fontColor(C.primary) }.width(100%) .onClick(() { this.showPreview !this.showPreview; }) if (this.showPreview) { Row({ space: 12 }) { Row() { Text().fontSize(36) } .width(60).height(60).backgroundColor(C.primarySoft).borderRadius(D.rMd) .justifyContent(FlexAlign.Center) Column({ space: 4 }) { Text(this.title.length 0 ? this.title : 拼单标题待填写) .fontSize(15).fontWeight(FontWeight.Medium).fontColor(C.text) .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis }) Row({ space: 8 }) { Text(分类: this.category).fontSize(11).fontColor(C.textDim) Text(¥ (this.price.length 0 ? this.price : 0)).fontSize(11).fontColor(C.danger) } Row({ space: 8 }) { Text(this.people 人).fontSize(11).fontColor(C.textSub) Text(this.deadline).fontSize(11).fontColor(C.warn) Text(this.delivery).fontSize(11).fontColor(C.textDim) } }.alignItems(HorizontalAlign.Start).layoutWeight(1) }.width(100%).padding(12).backgroundColor(C.cardSoft).borderRadius(D.rMd) } } .width(100%).padding(14).backgroundColor(C.card) .borderRadius(D.rLg).border({ width: 1, color: C.stroke }) }这是整个表单页最有仪式感的一张卡——它把用户填的所有信息打包成一张 GroupCard 缩略图让用户在下笔前就能看到最终效果。展开/收起通过this.showPreview控制。点击卡头任意位置切换状态——卡片顶部一行Row整体绑定了onClick意味着点击实时预览标题或展开 ▼提示都能触发切换。内容自适应标题为空时显示拼单标题待填写“占位文字单价为空时显示¥0”。这种空值兜底让预览卡永远不会出现空白或异常。信息聚合一行展示 2 个元数据分类、单价再一行展示 3 个流程元数据人数、截止、配送。这与首页 GroupCard 的三行信息结构完全一致——所以预览效果就是拼单发布后用户会看到的样子所见即所得。图标 emoji 写死为 这是 PreviewCard 的一处妥协——预览时所有品类都用奶茶图标。后续真实接入图片上传后可以根据this.category切换图标。十二、SubmitBtn表单校验的最后一关Builder SubmitBtn() { Button() { Column() { Text( 发布拼单).fontSize(16).fontWeight(FontWeight.Bold).fontColor(#FFFFFF) }.width(100%).height(50).justifyContent(FlexAlign.Center) } .type(ButtonType.Capsule).width(100%) .backgroundColor(C.primary) .onClick(() { if (this.title.length 0) { promptAction.showToast({ message: 请填写拼单标题 }); } else if (this.price.length 0) { promptAction.showToast({ message: 请填写单价 }); } else { promptAction.showToast({ message: 拼单已发布等待伙伴加入... }); } }) }发布按钮做了三件事样式圆角胶囊形 (ButtonType.Capsule)、主色背景、白字、加粗 emoji “”。这种主操作按钮 emoji的视觉处理让用户在长表单底部一眼就能定位到提交入口。校验onClick里先校验title再校验price。按优先级校验title 为空时优先提示请填写拼单标题title 不为空但 price 为空时提示请填写单价。这种按优先级校验的实现避免了一次性给用户 10 条错误信息导致认知过载。成功反馈校验通过后弹 toast “拼单已发布等待伙伴加入…”并保留 toast 显示时长让用户能完整看到成功消息。这里有一个渐进式校验的好实践——FieldCard 里的标题不能为空是实时校验用户能看到错误SubmitBtn 里的校验是提交校验兜底防止绕过实时校验。两者结合能覆盖 99% 的误操作场景。下图是拼单页底部可以看到实时预览收起状态和 发布拼单按钮可以看到更多选项的自动确认收货开关默认开启主色以及底部胶囊形的橙色 发布拼单按钮整张图把决策完成 → 提交确认的用户旅程完整呈现。十三、ArkTS 严格模式下的工程化细节Func1Tab同样严格遵循项目的代码规范接口先行Template接口显式声明了id、emoji、name、desc4 个字段所有templates数组中的对象必须符合接口。ForEach 第三参数必填所有ForEach都传了 keyGenerator如(t: Template) t.id.toString()、(cat: string) cat。无类型断言全文没有as any、as number、as string等逃生舱口。所有数据驱动 UI12 个State字段是页面唯一的事实来源UI 永远只是状态的映射。特别值得一提的是Builder的参数化每个卡片都是一个无参或小参的Builder比如PreviewCard接收this隐式状态GroupCard在首页接收g: GroupItem。这种统一的Builder风格让整个项目读起来非常一致。十四、可演进方向Func1Tab已经相当完整但要走向产品级还可以演进接入真实图片上传当前是占位 emoji。后续接入kit.MediaLibrary或自建上传服务把this.needImage替换为this.imageUrl: string | null。截止时间升级为 DatePicker当前是 4 个固定候选。后续可以让用户自由选择时间精度到分钟用 ArkUI 的DatePicker组件。价格输入限制price当前是字符串类型。后续可以加inputFilter限制只能输入数字和小数点并自动格式化为¥12.00。表单状态持久化用户在某个字段输入一半切到后台回来应该看到已填的内容。可以用AppStorage或 HarmonyOS 的应用状态管理实现。预览卡图标动态化根据this.category切换预览图标的 emoji。服务端草稿未发布的拼单保存为草稿用户下次打开自动恢复。十五、写在最后回到开篇的两张真机截图——从顶部 Header 到 8 张卡片、再到底部发布按钮整张拼单页用约 300 行 ArkTS 代码构建出了一个完整而优雅的表单工具。它最值得称道的地方是把选择型控件分类、人数、截止、配送、支付、自动确认和输入型控件标题、备注、单价、店铺清晰地分开——前者用 Chip / 圆按钮 / Toggle 处理后者用TextInput/TextArea处理再用一个实时预览把所有数据重新聚合展示。这种输入分散、聚合可见的模式让用户在填表过程中始终能感受到我做的事是有意义的。当你熟悉了StateonChangeif 条件渲染这三个核心概念后写一个完整的表单页面会比想象中轻松得多而像实时校验、所见即所得预览这些看不见的工程一旦做好就能让整个 App 的用户体验瞬间拉满。如果你也在做 HarmonyOS 表单开发不妨从这种卡片化 状态机的思路开始尝试。当你把表单页打磨到这种程度整个 App 的建环节就稳了。