ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

ArkTS 表单工程:场地预约页的三态场次 Grid 与校验

ArkTS 表单工程:场地预约页的三态场次 Grid 与校验 ArkTS 表单工程场地预约页的三态场次 Grid 与校验App 14「运动场地预约」场地 TabFunc1Tab是整 App 交互最丰富的页面——场地横向切换 三色图例 渐变预约预览卡 快捷模板 今日场次 Grid可选/已选/已满三态 预约信息表单 确认预约校验。本篇基于14-sports-booking/entry/src/main/ets/pages/Func1Tab.ets约 273 行逐段拆解附 4 张实机截图。一、整体结构9 个 Builder 块的预约漏斗Func1Tab 是 4 个 Tab 中 Builder 最多的页面9 个按选场地 → 看状态 → 选场次 → 填信息 → 确认的漏斗展开build() { Column() { this.Header() Scroll() { Column({ space: 16 }) { this.VenueRow() this.LegendRow() this.PreviewCard() this.QuickTemplateRow() this.SectionTitle(今日场次) this.SlotGrid() this.FormCard() 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) }8 个块不含 Header的叙事逻辑VenueRow— 选场地5 个胶囊标签横滑LegendRow— 看懂场次状态3 色图例PreviewCard— 当前选择预览渐变卡QuickTemplateRow— 快捷模板一键填选SectionTitle(今日场次)— 区块标题SlotGrid— 选场次8 个时段 2 列 GridFormCard— 填预约信息姓名/手机/备注SubmitBtn— 确认预约校验场次姓名漏斗设计每一步都让用户离完成预约更近一步。与 App 13 歌曲创建页Func1Tab结构类似但多了场次选择的交互复杂度。项目源码开源https://gitee.com/codenestFlow/HarmonyOSHub二、Header VenueRow场地切换胶囊Header 是简单的单行标题VenueRow 是 5 个场地的横向滚动胶囊标签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) } Builder VenueRow() { Scroll() { Row({ space: 10 }) { ForEach(this.venues, (v: string) { Text(v) .fontSize(13) .fontColor(this.venue v ? #FFFFFF : C.textSub) .padding({ left: 14, right: 14, top: 8, bottom: 8 }) .backgroundColor(this.venue v ? C.primary : C.card) .borderRadius(16) .border({ width: 1, color: this.venue v ? C.primary : C.stroke }) .onClick(() { this.venue v; this.selected ; }) }, (v: string) v) } } .scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off).width(100%) }Header 只有标题场地详情——本页是子页面性质从首页场地入口进来不需要首页那样的 Slogan。alignItems(VerticalAlign.Bottom)让标题靠底对齐height safeTop 5656vp 是标题区高度。VenueRow 的选中态三件套背景绿 vs 白、文字白 vs 灰、边框绿 vs 灰。5 个场地用Scroll横向滚动包裹未来加场地如游泳馆不会破版。关键交互细节onClick里同时更新两个 State.onClick(() { this.venue v; this.selected ; })切换场地时清空已选场次this.selected ——因为不同场地的场次不同切换后旧场次选择无效。这是表单联动里最常见的重置依赖状态逻辑容易漏但必须做。如果漏了用户会看到篮球场已选 19:00-20:00但切到羽毛球场后预览卡还显示这个场次——数据错乱。三、LegendRow三色图例LegendRow 是 3 个状态可选/已选/已满的图例说明Builder LegendRow() { Row({ space: 16 }) { Row({ space: 6 }) { Row().width(12).height(12).backgroundColor(#FFFFFF) .border({ width: 1, color: C.primary }).borderRadius(3) Text(可选).fontSize(12).fontColor(C.textSub) } Row({ space: 6 }) { Row().width(12).height(12).backgroundColor(C.primary).borderRadius(3) Text(已选).fontSize(12).fontColor(C.textSub) } Row({ space: 6 }) { Row().width(12).height(12).backgroundColor(C.textDim).borderRadius(3) Text(已满).fontSize(12).fontColor(C.textSub) } } .width(100%) }3 个色块 文字图例可选白底 绿色描边#FFFFFFborder C.primary已选绿底C.primary已满灰底C.textDim图例 颜色语义的说明文档。任何用颜色区分状态的 UI 都应该配图例否则新用户看不懂为什么这个格子是灰的。Row({ space: 16 })3 个图例之间 16vp 间距。每个图例是12×12 色块 6vp 文字。四、PreviewCard渐变预约预览卡PreviewCard 实时显示当前预约选择场地 场次是所见即所得的确认区Builder PreviewCard() { Row({ space: 12 }) { Column({ space: 6 }) { Text(预约预览).fontSize(11).fontColor(#FFFFFF).opacity(0.85) Text(this.venue).fontSize(18).fontWeight(FontWeight.Bold).fontColor(#FFFFFF) Text(this.selected.length 0 ? this.selected : 请选择场次).fontSize(13).fontColor(#FFFFFF).opacity(0.9) } .alignItems(HorizontalAlign.Start).layoutWeight(1) Column({ space: 4 }) { Text().fontSize(28) Text(this.selected.length 0 ? 已选 : 待选).fontSize(11).fontColor(#FFFFFF).opacity(0.85) } .alignItems(HorizontalAlign.Center) } .width(100%) .padding(16) .borderRadius(D.rLg) .linearGradient({ angle: 135, colors: [[C.primary, 0.0], [C.accent, 1.0]] }) }左侧三行预约预览小标签 场地名18sp 加粗 已选场次/请选择场次动态。右侧两行 时钟 emoji 已选/待选状态动态。动态状态切换Text(this.selected.length 0 ? this.selected : 请选择场次).fontSize(13).fontColor(#FFFFFF).opacity(0.9) ... Text(this.selected.length 0 ? 已选 : 待选).fontSize(11).fontColor(#FFFFFF).opacity(0.85)this.selected.length 0判断有没有选场次选了 → 显示场次如 19:00-20:00 右侧已选没选 → 显示请选择场次 右侧待选预览卡是状态驱动 UI的绝佳示例——State selected 一变三处文字同步更新预览场地名、场次文字、右侧状态。用户每点一个场次格子预览卡立刻响应所见即所得。五、QuickTemplateRow一键填选的快捷模板QuickTemplateRow 是 3 个快捷模板篮球晚场/羽毛球下午/乒乓球晨练的横向滚动卡片Builder QuickTemplateRow() { Column({ space: 10 }) { Text(快捷模板).fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text).width(100%) Scroll() { Row({ space: 10 }) { ForEach(this.templates, (t: Template) { Column({ space: 4 }) { Text(t.emoji).fontSize(24) Text(t.name).fontSize(11).fontColor(C.text) Text(t.slot).fontSize(10).fontColor(C.textDim) } .width(90) .padding({ top: 10, bottom: 10 }) .backgroundColor(C.cardSoft).borderRadius(D.rSm) .border({ width: 1, color: C.stroke }) .alignItems(HorizontalAlign.Center) .onClick(() { this.venue t.venue; this.selected t.slot; promptAction.showToast({ message: 已应用: t.name }); }) }, (t: Template) t.id.toString()) } } .scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off).width(100%) } .width(100%) }3 个模板数据 篮球晚场 — 篮球场 19:00-20:00 羽毛球下午 — 羽毛球场 14:00-15:00 乒乓球晨练 — 乒乓球场 08:00-09:00点击模板 一键填选.onClick(() { this.venue t.venue; this.selected t.slot; promptAction.showToast({ message: 已应用: t.name }); })一次点击同时更新venue场地和selected场次两个 State——模板的价值就是一键完成多个选择。比手动切场地 → 再点场次快 2 步。模板名称的时间语义晚场/下午/晨练匹配对应时段19:00-20:00 晚 / 14:00-15:00 下午 / 08:00-09:00 晨数据自洽——篮球晚场对应 19 点场次用户一看就懂。每个模板卡是 90vp 固定宽度的emoji 名称 时段三行卡C.cardSoft浅灰底 边框。六、SlotGrid2 列 Grid 场次网格三态切换核心SlotGrid 是整页最核心的交互——8 个时段的场次网格每个格子是可选/已选/已满三态Builder SlotGrid() { Grid() { ForEach(this.slots, (s: Slot) { GridItem() { Column({ space: 2 }) { Text(s.time).fontSize(12).fontColor(s.status 2 ? C.textDim : C.text) Text(s.status 2 ? 已满 : (this.selected s.time ? 已选 : 空闲)) .fontSize(10).fontColor(s.status 2 ? C.textDim : (this.selected s.time ? C.primary : C.ok)) } .width(100%).height(56) .justifyContent(FlexAlign.Center) .backgroundColor(s.status 2 ? C.cardSoft : (this.selected s.time ? C.primarySoft : #FFFFFF)) .borderRadius(D.rSm) .border({ width: 1, color: s.status 2 ? C.stroke : (this.selected s.time ? C.primary : C.primary) }) .opacity(s.status 2 ? 0.5 : 1.0) .onClick(() { if (s.status 2) { promptAction.showToast({ message: 该场次已满 }); return; } this.selected s.time; }) } }, (s: Slot, idx: number) s.time idx) } .columnsTemplate(1fr 1fr) .columnsGap(10) .rowsGap(10) .width(100%) }6.1 三态渲染逻辑每个格子的状态由两个因素决定场次本身的状态s.status0可选、2已满和是否被选中this.selected s.time。三态的组合渲染状态背景文字色说明文字可选status0未选中白 #FFFFFF深色 text空闲 绿色 ok已选status0选中浅绿 primarySoft主绿 primary已选 主绿已满status2浅灰 cardSoft灰 textDim已满 灰 50% 透明度两层嵌套三元Text(s.status 2 ? 已满 : (this.selected s.time ? 已选 : 空闲)) .fontSize(10).fontColor(s.status 2 ? C.textDim : (this.selected s.time ? C.primary : C.ok))先判断是否已满s.status 2再判断是否选中this.selected s.time。两层嵌套在 ArkTS 里是可读的2 层以内超过 3 层就该抽函数。opacity(s.status 2 ? 0.5 : 1.0)已满的格子整体降透明度到 50%——半透明让已满一眼可辨比纯灰色更有禁选感。6.2 点击逻辑已满拦截.onClick(() { if (s.status 2) { promptAction.showToast({ message: 该场次已满 }); return; } this.selected s.time; })已满的格子点击 → Toast 该场次已满 return不执行选中。可选格子点击 →this.selected s.time更新选中。注意 8 个时段的 status 分布08:00-09:00(0)、09:00-10:00(0)、10:00-11:00(2)、14:00-15:00(0)、15:00-16:00(0)、16:00-17:00(2)、17:00-18:00(0)、19:00-20:00(0)——第 3 和第 6 个格子已满上午 10 点和下午 4 点是高峰时段6 可选 2 已满让三态都展示。6.3 Grid 布局.columnsTemplate(1fr 1fr) .columnsGap(10) .rowsGap(10) .width(100%)2 列等宽1fr 1fr 行列间隙 10vp。8 个格子排成 4 行 2 列。Grid vs RowlayoutWeightGrid 适合规则网格RowlayoutWeight 适合单行等分。8 个场次用 Grid 更语义化。keyGenerator(s: Slot, idx: number) s.time idxs.time可能重复吗8 个时段不重复每个时间唯一但加上 idx 更保险——key 唯一性是 ForEach 的硬要求重复 key 会导致节点错位。七、FormCard预约信息表单FormCard 是姓名/手机/备注三行表单Builder FormCard() { Column({ space: 12 }) { Text(预约信息).fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text).width(100%) Row({ space: 8 }) { Text(姓名).fontSize(13).fontColor(C.textDim).width(50) TextInput({ placeholder: 请输入姓名, text: this.name }) .onChange((val: string) { this.name val; }) .backgroundColor(C.cardSoft).borderRadius(D.rSm).height(40) .layoutWeight(1).placeholderColor(C.textDim).placeholderFont({ size: 13 }) } .width(100%) Row({ space: 8 }) { Text(手机).fontSize(13).fontColor(C.textDim).width(50) TextInput({ placeholder: 请输入手机号, text: this.phone }) .onChange((val: string) { this.phone val; }) .backgroundColor(C.cardSoft).borderRadius(D.rSm).height(40) .layoutWeight(1).placeholderColor(C.textDim).placeholderFont({ size: 13 }) .type(InputType.Number) } .width(100%) Row({ space: 8 }) { Text(备注).fontSize(13).fontColor(C.textDim).width(50) TextInput({ placeholder: 选填, text: this.note }) .onChange((val: string) { this.note val; }) .backgroundColor(C.cardSoft).borderRadius(D.rSm).height(40) .layoutWeight(1).placeholderColor(C.textDim).placeholderFont({ size: 13 }) } .width(100%) } .width(100%) .padding(16) .backgroundColor(C.card).borderRadius(D.rMd) .border({ width: 1, color: C.stroke }) }每行 固定宽度标签50vp 弹性输入框layoutWeight(1)姓名— 普通文本输入手机—.type(InputType.Number)数字键盘手机号只需要数字弹数字键盘省去切换备注— 占位选填非必填标识InputType.Number是移动端表单的必备细节——手机号输入弹数字键盘用户体验提升明显。ArkUI 还支持InputType.PhoneNumber带拨号符号、InputType.Email带、InputType.Password密码打码等类型。三个 Statename/phone/note分别绑定三个输入框双向绑定模式与 App 13 一致。标签左对齐固定宽度Text(姓名).width(50)让三行的标签右边缘对齐输入框左边缘也对齐——标签固定宽 输入弹性宽是表单的标准布局。八、SubmitBtn双重校验的确认预约SubmitBtn 是确认预约按钮带场次姓名双重校验Builder SubmitBtn() { Button(确认预约) .width(100%).height(48) .fontSize(16).fontColor(#FFFFFF).backgroundColor(C.primary) .borderRadius(D.rMd) .onClick(() { if (this.selected.length 0) { promptAction.showToast({ message: 请选择场次 }); return; } if (this.name.length 0) { promptAction.showToast({ message: 请输入姓名 }); return; } promptAction.showToast({ message: this.venue this.selected 预约成功 }); }) }双重早返回校验.onClick(() { if (this.selected.length 0) { promptAction.showToast({ message: 请选择场次 }); return; } if (this.name.length 0) { promptAction.showToast({ message: 请输入姓名 }); return; } promptAction.showToast({ message: this.venue this.selected 预约成功 }); })先验场次selected.length 0→ 请选择场次再验姓名name.length 0→ 请输入姓名全过 → 预约成功篮球场 19:00-20:00 预约成功动态拼接校验顺序有讲究先验场次流程必填再验姓名表单必填——先提醒用户你还没选核心东西再提醒表单没填完。如果反过来用户会先看到请输入姓名但还搞不清要选场次。成功提示动态拼接this.venue this.selected 预约成功把用户的选择完整回显——篮球场 19:00-20:00 预约成功用户确认我约的是这个没错。与 App 13 的差异App 13 提交按钮有动态文案✓ 提交创建/请填写标题App 14 按钮固定确认预约但校验内容更完整2 项校验 vs 1 项。两种设计都对——动态文案省一次点击固定文案更稳重。九、State 的设计5 个状态覆盖整页交互Func1Tab 有 5 个 StateState类型用途被谁更新venuestring当前场地VenueRow 点击 / 模板点击selectedstring已选场次SlotGrid 点击 / 模板点击 / 切换场地清空namestring姓名FormCard 输入phonestring手机号FormCard 输入notestring备注FormCard 输入selected是最核心的状态——它同时驱动预览卡显示已选场次、SlotGrid格子高亮、SubmitBtn校验是否为空。一个状态多处渲染是响应式 UI 的本质。venue的状态联动切换场地 → 清空 selectedthis.selected → 预览卡变请选择场次→ SlotGrid 无高亮 → 提交校验失败提示请选择场次。一条状态链贯穿整页是状态驱动的教科书案例。十、9 个 Builder 的编排价值Func1Tab 有 9 个 BuilderHeader/VenueRow/LegendRow/PreviewCard/QuickTemplateRow/SectionTitle/SlotGrid/FormCard/SubmitBtn是 4 个 Tab 中最多的。这种拆分带来每个 Builder 可独立理解——SlotGrid 看网格逻辑FormCard 看表单逻辑互不干扰状态变更局部化——selected只在 SlotGrid/PreviewCard 用改 SlotGrid 不用动 FormCard复用潜力——VenueRow/LegendRow/SlotGrid 这套场地选择器可整体抽成组件供其他页面如团体预约复用但 9 个 Builder 也有代价页面顶部堆了太多方法找某个逻辑要滚动很久。如果 Func1Tab 继续膨胀比如加团体预约建议把场地选择器VenueRowLegendRowSlotGrid和预约表单FormCardSubmitBtn拆成独立 Component。十一、与 App 13 歌曲创建页的对比App 13 Func1Tab歌曲创建和 App 14 Func1Tab场地预约是同源的创建页但有几个关键差异维度App 13 歌曲创建App 14 场地预约主题音乐运动核心交互类型单选 优先级单选场地切换 场次三态网格特殊组件Toggle 开关Grid 网格 图例校验项标题 1 项场次 姓名 2 项快捷模板3 条列表3 条横滑卡预览无渐变预览卡App 14 的预览卡和场次网格是新增的交互模式——前者所见即所得后者网格化多选。这套场地预约表单模板可以直接复用到会议室预约/工位预约/座位预约等场景。到此App 14「运动场地预约」场地预约页解析完毕。场地切换 三态网格 预览反馈 双重校验构成一个完整、有说服力的预约流程。
返回列表