十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

ArkTS 表单工程:快递寄件页的包裹大小选择与保价开关实时预览

ArkTS 表单工程:快递寄件页的包裹大小选择与保价开关实时预览 ArkTS 表单工程快递寄件页的包裹大小选择与保价开关实时预览应用背景11 校园快递campus-express第二个 Tab 是寄件页对应源码Func1Tab.ets共 280 行在Index.ets的TabContent里被标签为查询。它是用户发起寄件的核心入口橙色主题#EA580C。本文逐行拆解其AddressCard 收寄地址卡、TypeSelector 寄件方式、SizeSelector 包裹大小、InsuredSwitch 保价开关、PreviewCard 实时预览、TemplateSection 快捷模板、SubmitBtn 提交七大模块重点讲解声明式表单里选项态如何驱动 UI 变化。寄件页和首页的气质完全不同首页是看寄件页是填。它本质上是一张长表单——收件人、寄件方式、包裹大小、是否保价、运费预览、模板选择每一项都是一次状态 → 视图的映射。读懂寄件页就读懂了 ArkUI 里选择器类控件的标准写法。表单类页面是移动开发的基本功几乎所有 App 都至少有一处长表单注册、下单、设置、反馈。而 ArkUI 的声明式范式让表单比其他框架更轻——你不用手写setState再手动刷新 DOM只要把State绑到控件上UI 就会自动跟随。11 号寄件页把这种轻发挥到了极致七个区块、四个状态变量全程没有一行命令式刷新代码。后面你会看到所谓交互很多时候只是改一个状态变量这么简单。一、数据模型与页面骨架寄件页只定义了两个接口比首页精简得多因为它的数据大多内联在组件里interface TemplateItem { id: number; emoji: string; name: string; price: string; desc: string; } interface PreviewItem { label: string; value: string; }TemplateItem描述快捷模板里的一张卡片图标、名称、价格、描述。PreviewItem则是预览区的一行键值对。其余诸如寄件方式、包裹大小作者直接用private types: string[]与private sizes: string[]数组表达因为这两个字段不需要附加属性纯字符串数组更省事。页面骨架是一个Column上面是Header()下面是Scroll()。注意Scroll内部Column({ space: 16 })的space比首页的 14 略大——因为表单区块之间需要更明显的分隔避免用户把寄件方式和包裹大小误读成同一组。build() { Column() { this.Header() Scroll() { Column({ space: 16 }) { this.AddressCard() this.TypeSelector() this.SizeSelector() this.InsuredSwitch() this.PreviewCard() this.TemplateSection() 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) }同样地safeBottom出现在底部padding里保证提交按钮在滚动到底时不会被底部导航条遮住。State声明了 4 个交互状态typeIdx寄件方式下标、sizeIdx包裹大小下标、senderName/receiverName两个输入框文本、insured保价布尔。它们就是整张表单的数据中枢。这里有个表单设计的通用原则值得点破把所有会变的都收进State把所有不变的都写死成常量或内联。寄件页严格遵循了这点——types、sizes、templates、previewItems等数据虽然写在组件里但它们不参与交互点了也不会改自己所以不需要状态真正需要响应用户的是那 4 个变量。当你面对一张复杂表单时先问自己哪些字段会被用户改变只把这些变成状态其余一律静态——状态越少重绘范围越小、Bug 越少。项目源码开源https://gitee.com/codenestFlow/HarmonyOSHub二、Header 标题栏// 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) }这里用了一个很巧的高度写法.height(this.safeTop 56)。相比首页 Header 把safeTop放进padding这里直接把safeTop算进整体高度padding只留top: safeTop配合alignItems(VerticalAlign.Bottom)让寄件标题始终贴在标题栏的底部——也就是刚好贴在状态栏下方那条分界线上。两种写法殊途同归但高度含 safeTop 内容底部对齐在需要标题精确贴底时更直观。另一个细节是VerticalAlign.Bottom与padding({ top: this.safeTop })的配合因为整条 Row 的高度已经是safeTop 56而顶部又留了safeTop的 padding所以真正的内容区只剩底部 56vp标题恰好落在状态栏正下方那道内容基线上。如果换成VerticalAlign.Center标题会整体上移在刘海屏上可能和状态栏文字重叠。这种用对齐方式而非 magic number 定位内容的思路比写死padding-top: 40更健壮——设备一换写死的数值就崩而基于 safeTop 的计算永远正确。三、AddressCard 收寄地址卡这是表单里最像真实业务的一块左侧一个圆形徽章标寄/收右侧是姓名输入框 楼栋地址文字中间用Divider分隔两组。// AddressCard — 收寄地址卡 Builder AddressCard() { Column({ space: 12 }) { Row({ space: 12 }) { Column() { Text(寄).fontSize(14).fontColor(#FFFFFF).fontWeight(FontWeight.Bold) } .width(28).height(28) .backgroundColor(C.primary).borderRadius(14) .justifyContent(FlexAlign.Center) Column({ space: 4 }) { TextInput({ placeholder: 寄件人姓名, text: this.senderName }) .backgroundColor(transparent).borderRadius(D.rSm).height(36) .placeholderColor(C.textDim).placeholderFont({ size: 13 }) .onChange((val: string) { this.senderName val; }) Text(梅园 6 栋 302 室).fontSize(11).fontColor(C.textDim) } .alignItems(HorizontalAlign.Start).layoutWeight(1) }.width(100%) Divider().color(C.stroke).strokeWidth(0.5) Row({ space: 12 }) { Column() { Text(收).fontSize(14).fontColor(#FFFFFF).fontWeight(FontWeight.Bold) } .width(28).height(28) .backgroundColor(C.accent).borderRadius(14) .justifyContent(FlexAlign.Center) Column({ space: 4 }) { TextInput({ placeholder: 收件人姓名, text: this.receiverName }) .backgroundColor(transparent).borderRadius(D.rSm).height(36) .placeholderColor(C.textDim).placeholderFont({ size: 13 }) .onChange((val: string) { this.receiverName val; }) Text(点击填写收件地址).fontSize(11).fontColor(C.primary) .onClick(() { promptAction.showToast({ message: 选择地址 }); }) } .alignItems(HorizontalAlign.Start).layoutWeight(1) }.width(100%) } .width(100%).padding(14) .backgroundColor(C.card).borderRadius(D.rLg) .border({ width: 1, color: C.stroke }) }三个细节其一寄用主色C.primary圆点收用强调色C.accent圆点用颜色区分收发双方比用两个相同灰点更直观其二两个TextInput都用backgroundColor(transparent)融进卡片且通过onChange双向写回State这是 ArkUI 表单的标准数据绑定其三收件地址那行小字是C.primary橙色且可点点它弹选择地址Toast——暗示真实场景会跳地址簿但 demo 阶段用 Toast 占位保持单页不跳转。Divider().color(C.stroke).strokeWidth(0.5)是 AddressCard 里很见功力的一笔它用一条极细0.5vp的浅色线把寄和收两组信息切开比再用一个卡片包裹更轻。注意Divider默认是横向满宽strokeWidth(0.5)在 3x 屏上正好接近 1 物理像素避免线过粗显脏。寄件页里寄/收两块共用一个外层卡片、用 Divider 内分而不是拆成两个卡片——这减少了视觉层级让用户一眼看出这是同一笔寄件的收发双方。四、TypeSelector 寄件方式寄件方式用ForEach渲染三个选项每个选项由 emoji 文字组成选中态用背景色翻转来表现。// TypeSelector — 寄件方式选择 Builder TypeSelector() { Column({ space: 12 }) { Text(寄件方式).fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text).width(100%) Row({ space: 10 }) { ForEach(this.types, (t: string, idx: number) { Column({ space: 4 }) { Text(idx 0 ? : idx 1 ? ⚡ : ).fontSize(24) Text(t).fontSize(13).fontColor(this.typeIdx idx ? #FFFFFF : C.textSub) } .layoutWeight(1) .padding({ top: 14, bottom: 14 }) .backgroundColor(this.typeIdx idx ? C.primary : C.card) .borderRadius(D.rMd) .border({ width: 1, color: this.typeIdx idx ? C.primary : C.stroke }) .onClick(() { this.typeIdx idx; }) }, (t: string) t) }.width(100%) } .width(100%).padding(14) .backgroundColor(C.card).borderRadius(D.rLg) .border({ width: 1, color: C.stroke }) }选中态的核心是一行三元表达式this.typeIdx idx ? C.primary : C.card同时控制背景色和边框色文字色用this.typeIdx idx ? #FFFFFF : C.textSub。于是未选中时是白底灰字描边选中时变成橙底白字实边框——反差足够强用户一眼能看出当前选了哪个。emoji 也用idx 0 ? : ...做了内联映射标准、特快⚡、经济用图标语义强化速度感。点击任意选项只做一件事this.typeIdx idx状态一变所有依赖typeIdx的fontColor/backgroundColor自动重算重绘——这就是声明式 UI 状态驱动视图 最朴素的体现。需要特别留意ForEach的第三个参数(t: string) t是键生成函数它必须返回能唯一标识每条数据的 key。这里用字符串本身当 key寄件方式三个值不重复所以直接返回t。如果数组里有重复项就必须改用下标或 id否则 ArkUI 在增删数据时会复用错节点、闹出点 A 却高亮 B的诡异 Bug。选单类控件几乎都跑不掉ForEach把 key 规则想清楚能省掉一大半列表状态错乱的调试时间。五、SizeSelector 包裹大小包裹大小用Flex换行布局做标签云式选择逻辑和 TypeSelector 如出一辙但视觉形态从等宽平铺变成自适应宽度。// SizeSelector — 包裹大小选择 Builder SizeSelector() { Column({ space: 12 }) { Text(包裹大小).fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text).width(100%) Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Start }) { ForEach(this.sizes, (s: string, idx: number) { Text(s) .fontSize(15).fontWeight(FontWeight.Medium) .fontColor(this.sizeIdx idx ? #FFFFFF : C.text) .padding({ left: 20, right: 20, top: 12, bottom: 12 }) .backgroundColor(this.sizeIdx idx ? C.primary : C.cardSoft) .borderRadius(D.rSm) .border({ width: 1, color: this.sizeIdx idx ? C.primary : C.stroke }) .margin({ right: 10, bottom: 10 }) .onClick(() { this.sizeIdx idx; }) }, (s: string) s) }.width(100%) } .width(100%).padding(14) .backgroundColor(C.card).borderRadius(D.rLg) .border({ width: 1, color: C.stroke }) }差别在于外层用Flex({ wrap: FlexWrap.Wrap })而非Row当选项多到一行放不下时会自动换行每个标签用margin({ right: 10, bottom: 10 })留出间距。未选中背景是C.cardSoft比纯白更柔和的浅色选中才变C.primary——比 TypeSelector 的白底 vs 橙底更克制因为包裹大小是次要维度不该抢走寄件方式的视觉权重。这种主选项强反差、次选项弱反差的分级正是多组选择器共存时的排布心法。Flex({ wrap: FlexWrap.Wrap })还有个隐藏好处它天然响应式。在窄屏上文件袋/纸箱/木箱可能挤成两行在更宽的设备上又自动排成一行开发者完全不用写媒体查询。配合margin({ right: 10, bottom: 10 })换行的标签之间也保持均匀间距不会出现最后一行贴左边、右边空一大块的失衡。当你需要标签数量不定、且要自动换行的布局时FlexWrap 比 Grid 或 Row 都更省心。六、InsuredSwitch 保价开关保价是一条开关型配置用 ArkUI 内置Toggle实现比手搓一个可点矩形优雅太多。// InsuredSwitch — 保价服务开关 Builder InsuredSwitch() { Row({ space: 12 }) { Column({ space: 4 }) { Text(保价服务).fontSize(14).fontWeight(FontWeight.Medium).fontColor(C.text) Text(贵重物品建议保价).fontSize(11).fontColor(C.textDim) } .alignItems(HorizontalAlign.Start).layoutWeight(1) Toggle({ type: ToggleType.Switch, isOn: this.insured }) .selectedColor(C.primary) .onChange((on: boolean) { this.insured on; promptAction.showToast({ message: on ? 已开启保价 : 已关闭保价 }); }) } .width(100%).padding(14) .backgroundColor(C.card).borderRadius(D.rLg) .border({ width: 1, color: C.stroke }) }Toggle的isOn直接绑定State insuredselectedColor(C.primary)让开启时的滑块轨道变成品牌橙。onChange里同时更新状态和弹 Toast——这是开关 即时反馈的范式。左侧用标题 辅助说明两行小字解释保价意义符合 iOS/Android 里开关右侧配说明文字的通用布局。selectedColor(C.primary)只控制了开启状态的轨道色而滑块本身始终是白色——这是 ArkUIToggle的默认行为你没法直接用属性改滑块色但可以通过自定义ToggleType.Button或自己手搓开关来完全定制。对绝大多数场景selectedColor已经够用它让开的状态带上品牌色用户在视觉上能获得清晰的正反馈。值得一提的是保价开关的状态insured目前没有参与任何计算但它在真实项目里正是运费预览里¥12那个数字要不要加保价费的开关——把这两个点连起来就是状态联动的完整闭环。七、PreviewCard 实时运费预览预览区把用户前面选的各项汇总成一张订单小票最关键的是最后一行实付运费用红色高亮。// PreviewCard — 寄件预览 Builder PreviewCard() { Column({ space: 12 }) { Text(寄件预览).fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text).width(100%) ForEach(this.previewItems, (p: PreviewItem) { Row() { Text(p.label).fontSize(13).fontColor(C.textDim) Blank() Text(p.value).fontSize(13).fontColor(C.text).fontWeight(FontWeight.Medium) }.width(100%) }, (p: PreviewItem) p.label) Divider().color(C.stroke).strokeWidth(0.5) Row() { Text(实付运费).fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text) Blank() Text(¥12) .fontSize(22).fontColor(C.danger).fontWeight(FontWeight.Bold) }.width(100%) } .width(100%).padding(14) .backgroundColor(C.card).borderRadius(D.rLg) .border({ width: 1, color: C.stroke }) }每一行都是标签 Blank() 值的左右对齐结构Blank()是 ArkUI 的弹簧组件自动把后面的文字推到最右比手写layoutWeight更语义化。Divider把明细和总计切开最后一行实付运费 ¥12用C.danger红放大到 22 号字——红色在这里承担价格注意的暗示引导用户确认金额。注意预览数据在 demo 里是固定写死的真实项目中应当随typeIdx/sizeIdx/insured的变化动态计算这正是状态驱动 UI 可以自然延伸的方向。实现动态计算其实很小把previewItems从常量改为State在typeIdx/sizeIdx/insured的onClick/onChange里顺手重算一份新数组赋回去即可。由于 ArkUI 会对State数组做浅比较记得用this.previewItems [...新数组]生成新引用否则界面不刷新。这条经验在表单实时汇总购物车小计、订单总价、BMI 计算结果里无处不在——本质都是输入状态 → 派生数据 → 渲染汇总的同一条数据流。八、TemplateSection 快捷模板模板区把常见寄件场景寄同学、寄礼物、寄教材、寄衣物做成可点卡片点一下即可一键填充。// TemplateSection — 快捷模板 Builder TemplateSection() { Column({ space: 12 }) { Text(快捷模板).fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text).width(100%) ForEach(this.templates, (t: TemplateItem) { Row({ space: 12 }) { Row() { Text(t.emoji).fontSize(22) } .width(42).height(42) .backgroundColor(C.primarySoft).borderRadius(D.rSm) .justifyContent(FlexAlign.Center) Column({ space: 3 }) { Text(t.name).fontSize(14).fontColor(C.text).fontWeight(FontWeight.Medium) Text(t.desc).fontSize(11).fontColor(C.textDim) } .alignItems(HorizontalAlign.Start).layoutWeight(1) Text(t.price).fontSize(15).fontColor(C.primary).fontWeight(FontWeight.Bold) } .width(100%).padding({ left: 12, right: 12, top: 10, bottom: 10 }) .backgroundColor(C.cardSoft).borderRadius(D.rSm) .onClick(() { promptAction.showToast({ message: 使用模板: t.name }); }) }, (t: TemplateItem) t.id.toString()) } .width(100%).padding(14) .backgroundColor(C.card).borderRadius(D.rLg) .border({ width: 1, color: C.stroke }) }每条模板是emoji 图标 名称描述 价格的三段式图标用C.primarySoft主色的浅色版做底价格用C.primary橙色强调。整条可点弹使用模板: xxxToast。这里体现了模板的产品价值把高频寄件场景预置好用户不必每次重填是表单页常见的提效手段。背景用C.cardSoft而非C.card让模板卡与外层白卡形成微妙的层叠感。模板本质上是预设的状态组合比如寄同学模板可能意味着typeIdx0标准sizeIdx1中件insuredfalse。真实项目里点模板时应当一次性把这几个状态变量都改掉页面上的寄件方式、包裹大小、保价开关会同步跳到模板对应的样子——这就是用一次点击修改多个状态的典型场景。11 号 demo 用 Toast 占位但你已经能从架构上看出只要把onClick里的showToast换成this.typeIdx ...; this.sizeIdx ...;模板填充就活了。九、SubmitBtn 提交按钮表单末尾是一个通栏的主操作按钮。// SubmitBtn — 确认寄件 Builder SubmitBtn() { Button(确认寄件) .width(100%).height(50) .fontSize(16).fontColor(#FFFFFF).backgroundColor(C.primary) .borderRadius(D.rMd) .onClick(() { promptAction.showToast({ message: 寄件成功 }); }) }Button直接设width(100%)通栏、backgroundColor(C.primary)品牌橙、白字点击弹寄件成功。它和 HeroCard 里的扫码取件按钮共享同一套视觉语言——橙色实底白字 主操作这种跨页面的一致性正是C主题类存在的意义。真正上线时提交按钮里应当先跑一轮表单校验检查senderName/receiverName非空、收件地址已选、寄件方式已选任一不满足就promptAction.showToast提示具体缺哪样全部通过才发起网络请求。把校验逻辑集中放在SubmitBtn的onClick开头配合前面状态集中在 State的设计校验会变得极其简单——你只需要读那 4 个变量而不用在 DOM 里翻找输入框。这就是状态集中带来的第二个红利校验与提交共享同一份数据源。十、与首页表单化程度的对比寄件页和首页是看 vs 填的两极首页几乎没有可编辑状态唯一的状态searchText还不参与任何计算寄件页则把State用到了极致——4 个状态变量驱动着 7 个区块中至少 5 个的视觉变化。这种差异让两个页面在代码密度上截然不同首页重在布局与配色寄件页重在状态与交互。把两者对照着学能快速建立声明式 UI 里状态如何流动的直觉首页教你怎么把数据画漂亮寄件页教你怎么让点击改变画面。如果硬要给两者排个进阶顺序建议先吃透首页布局与配色再啃寄件页状态与交互。因为布局是静态的地基状态是动态的上层地基稳了上层建筑才立得住。当你能把首页的画和寄件页的变都讲清楚ArkUI 声明式开发的任督二脉基本就通了——后面取件页、我的页无非是在这两个能力上做组合与变奏。十一、本章小结与工程经验组件核心技术学习价值AddressCard圆形徽章 TextInput 双向绑定真实业务表单结构TypeSelectoridxtypeIdx反差选中等宽单选器标准写法SizeSelectorFlexWrap.Wrap标签云自适应宽度单选InsuredSwitchToggle开关内置开关控件用法PreviewCardBlank()右对齐 红价订单小票式汇总TemplateSection图标 描述 价格三段式快捷模板提效SubmitBtn通栏主按钮跨页一致的视觉语言最大收获是选择器三板斧用State存下标 → 用ForEach渲染选项 → 在样式里写idx stateIdx ? 选中样式 : 未选中样式点击只改下标。这套模式在寄件方式、包裹大小上完全一致可一键复用到任何单选卡片场景。其次Toggle、Progress、Blank、Divider等内置控件的大量使用再次印证了能用系统组件就别手搓的原则既减代码又保一致。把寄件页浓缩成一句话它是一堂状态驱动视图的实战课。七个区块共享 4 个状态每个选择器都是下标存状态 ForEach 渲染 样式三元切换 点击改下标的四步曲。你把这个四步曲背下来市面上一半的表单类页面都能照葫芦画瓢再补上状态集中便于校验模板即预设状态组合两条心法剩下一半也大体逃不出这个框架。
返回列表