鸿蒙原生应用 ArkTS 严格模式:预约页的 @State 真单选与动态回显
App 23「实验室设备预约」预约 Tab(Func2Tab),是预约时段选择页——
@State slot真单选(4 时段 Flex 换行胶囊:09:00-11:00 默认选中)+ 确认预约按钮("预约成功: 明天 09:00-11:00"动态回显)+ 预约记录列表(4 条:待使用橙/已完成绿)。本篇基于23-lab-booking/entry/src/main/ets/pages/Func2Tab.ets(约 126 行)逐段拆解,附 4 张实机截图。
一、整体结构:选时段 → 提交 → 看记录
Func2Tab 是"选时段 → 确认 → 看历史"的预约流程:
build() { Column() { this.Header() Scroll() { Column({ space: 16 }) { this.SectionTitle('选择时段') this.SlotCard() this.SubmitBtn() this.SectionTitle('预约记录') this.BookingList() } .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) }5 个块按"选 → 提交 → 看"漏斗展开:
- SectionTitle('选择时段')— 区块标题
- SlotCard— 4 时段 Flex 单选(本页核心交互)
- SubmitBtn— 确认预约(动态回显时段)
- SectionTitle('预约记录')— 区块标题
- BookingList— 4 条预约记录(待使用橙/已完成绿)
与 App 14 场地预约的对比:App 14 是"场地选择 + 场次 Grid 三态",App 23 是"4 时段 Flex 单选"——App 23 更简单(4 个时段、单选、无三态)——"实验室预约 = 更简单的时段选择"。
项目源码开源:https://gitee.com/codenestFlow/HarmonyOSHub
二、Header + SectionTitle
Header 是"设备预约"单行(系列标准)。SectionTitle 是参数化标题("选择时段"/"预约记录"两个区块):
@Builder SectionTitle(title: string) { Row() { Text(title).fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text) } .width('100%') }两个 SectionTitle 分隔"操作区"(选时段+提交)和"记录区"(预约记录)——"区块标题 = 操作/历史的分界"。
三、SlotCard:4 时段 Flex 真单选(本页核心)
SlotCard 是 4 个时段胶囊的Flex 换行 + 真单选:
@Builder SlotCard() { Column({ space: 12 }) { Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Start }) { ForEach(this.slots, (s: string) => { Text(s) .fontSize(13) .fontColor(this.slot === s ? C.primary : C.textSub) .padding({ left: 14, right: 14, top: 8, bottom: 8 }) .backgroundColor(this.slot === s ? C.primarySoft : C.cardSoft) .borderRadius(16) .border({ width: 1, color: this.slot === s ? C.primary : C.stroke }) .margin({ right: 10, bottom: 10 }) .onClick(() => { this.slot = s; }) }, (s: string) => s) } .width('100%') } .width('100%') .padding(14) .backgroundColor(C.card) .borderRadius(D.rLg) .border({ width: 1, color: C.stroke }) }3.1 4 时段数据
@State slot: string = '09:00-11:00'; private slots: string[] = ['09:00-11:00', '11:00-13:00', '14:00-16:00', '16:00-18:00'];4 个时段(连续覆盖一天):
- 09:00-11:00(上午)
- 11:00-13:00(午间)
- 14:00-16:00(下午)
- 16:00-18:00(傍晚)
`@State slot: string = '09:00-11:00' 默认选中第一时段——"默认选第一个"是时段选择的标准(减少用户操作)。
3.2 真单选交互
onClick(() => { this.slot = s; })—— 点击时段 →slot更新 ——4 个胶囊的选中态实时变化:
Text(s) .fontSize(13) .fontColor(this.slot === s ? C.primary : C.textSub) .backgroundColor(this.slot === s ? C.primarySoft : C.cardSoft) .borderRadius(16) .border({ width: 1, color: this.slot === s ? C.primary : C.stroke })选中态三件套:
- 文字:深青 vs 灰
- 背景:浅青(
C.primarySoft)vs 浅灰(C.cardSoft) - 边框:深青 1vp vs 浅灰 1vp
"真单选" = 点击即切换 + 提交按钮回显(App 22 投票是真单选 + 锁定;App 23 是真单选 + 回显但无锁定)——"真单选"是"@State 状态驱动选中"的实现。
3.3 Flex 换行
Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Start })—— 4 个时段自动换行(4 个在窄屏可能 1 行放不下)——margin({ right: 10, bottom: 10 })胶囊间距。
justifyContent(FlexAlign.Start)—— 从左上角排列(不居中)——"换行内容左对齐"是 Flex 布局的标准。
四、SubmitBtn:动态回显时段
SubmitBtn 是"确认预约"按钮,成功提示回显当前选中时段:
@Builder SubmitBtn() { Button('确认预约') .width('100%').height(48) .fontSize(16).fontColor('#FFFFFF').backgroundColor(C.primary) .borderRadius(D.rMd) .onClick(() => { promptAction.showToast({ message: '预约成功: 明天 ' + this.slot }); }) }'预约成功: 明天 ' + this.slot—— 成功提示拼接当前时段("预约成功: 明天 14:00-16:00")——"动态回显"让用户确认"我约的是这个时段"——与 App 14 场地预约"篮球场 19:00-20:00 预约成功"同款"回显"。
this.slot从 @State 读取—— 用户选了哪个时段,提交就回显哪个——"@State 状态驱动"。
与 App 14 的差异:App 14 有"场次+姓名"双校验,App 23无校验(时段默认已选,无需校验)——**"默认选中 = 免校验"**是"默认值设计"的好处。
五、BookingList:预约记录列表
BookingList 是 4 条预约记录(待使用橙/已完成绿):
@Builder BookingList() { Column({ space: 12 }) { ForEach(this.bookings, (b: Booking) => { Row({ space: 12 }) { Text(b.device).fontSize(28) Column({ space: 4 }) { Text(b.date + ' ' + b.slot).fontSize(14).fontColor(C.text) Text(b.status).fontSize(12).fontColor(b.statusColor) } .alignItems(HorizontalAlign.Start).layoutWeight(1) } .width('100%') .padding(14) .backgroundColor(C.card).borderRadius(D.rMd) .border({ width: 1, color: C.stroke }) .onClick(() => { promptAction.showToast({ message: b.device }); }) }, (b: Booking) => b.id.toString()) } .width('100%') }5.1 4 条记录的数据
private bookings: Booking[] = [ { id: 1, device: '🔬 高倍显微镜', date: '今天', slot: '14:00-16:00', status: '待使用', statusColor: C.warn }, { id: 2, device: '⚡ 示波器', date: '明天', slot: '09:00-11:00', status: '待使用', statusColor: C.warn }, { id: 3, device: '🧪 液相色谱仪', date: '昨天', slot: '14:00-16:00', status: '已完成', statusColor: C.ok }, { id: 4, device: '🔭 光谱分析仪', date: '8月10日', slot: '11:00-13:00', status: '已完成', statusColor: C.ok } ];4 条记录的"设备 + 日期 + 时段 + 状态":
| 设备 | 日期 | 时段 | 状态 |
|---|---|---|---|
| 🔬 高倍显微镜 | 今天 | 14:00-16:00 | 待使用(橙) |
| ⚡ 示波器 | 明天 | 09:00-11:00 | 待使用(橙) |
| 🧪 液相色谱仪 | 昨天 | 14:00-16:00 | 已完成(绿) |
| 🔭 光谱分析仪 | 8月10日 | 11:00-13:00 | 已完成(绿) |
2 待使用 + 2 已完成——"待使用 = 未来预约、已完成 = 历史使用"——"预约记录的生命周期"。
2 状态 2 色:待使用(橙)/已完成(绿)——"待办用橙、完成用绿"(系列标准)。
记录卡 2 列:左 emoji + 右"日期+时段"和"状态"——"设备 + 时间 + 状态"3 信息。
5.2 与 Func1Tab 近期预约的"跨页对应"
预约页记录(今天 14:00-16:00 待使用)= 设备详情页近期预约(今天 14:00-16:00 已被预约)——"设备详情页的近期预约 = 预约页的记录"——跨页数据一致(同一时段同一状态)。
六、@State 与"预约流程"闭环
Func2Tab 只有1 个 @State:slot(选中时段)——"1 个 @State 驱动时段选中 + 提交回显"。
预约流程闭环:
首页(找设备)→ 设备详情(看参数)→ 预约页(选时段 + 确认)→ 记录(看历史)"找 → 看 → 选 → 记"4 步预约闭环——App 23 的 4 个 Tab 承担 4 步:首页(找)、设备(看)、预约(选)、我的(记)——"Tab 分工 = 业务闭环"。
七、时段选择的"业务规则"设计
App 23 的 4 个时段(09:00-11:00 / 11:00-13:00 / 14:00-16:00 / 16:00-18:00)是"连续时段"——真实预约系统的"时段规则"有 3 种:
1. 固定时段(App 23)——系统预设 4 个时段,用户选一个——简单、可控、防冲突(适合实验室统一管理)。
2. 自由时间——用户输入起止时间(如 10:30-12:00)——灵活、但冲突检测复杂(需实时校验重叠)。
3. 时长预约——用户选"开始时间 + 使用时长"(14:00 开始用 2 小时)——介于两者之间(起点自由、时长固定)。
"固定时段 vs 自由时间"的取舍:
- 固定时段:实现简单、管理规范、用户选择成本低(4 选 1)
- 自由时间:用户灵活、但需要时间选择器 + 实时冲突检测
实验室场景适合"固定时段"——实验室设备统一管理,固定时段方便调度和统计(每个时段 2 小时是标准的"实验课"时长)。
真实项目的时段配置:
// 时段从配置读取,可按设备配置不同时段 private getSlots(deviceId: number): string[] { return slotConfig[deviceId] ?? ['09:00-11:00', '11:00-13:00', '14:00-16:00', '16:00-18:00']; }"时段可配置"——不同设备不同时段(精密仪器晚间可用、普通设备白天可用)——"业务规则可配置"是预约系统的灵活性。
八、"预约记录"的状态机与操作
App 23 预约记录有 2 状态(待使用/已完成)——真实预约记录有完整状态机:
待使用(已预约,还没用) → 使用中(开始使用) → 已完成(使用结束) ↘ 已取消(用户取消) ↘ 已过期(没来也没取消)5 状态的 UI 差异:
| 状态 | 颜色 | 操作 |
|---|---|---|
| 待使用 | 橙 | 取消预约 |
| 使用中 | 蓝/主色 | 结束使用 |
| 已完成 | 绿 | 查看评价 |
| 已取消 | 灰 | 无 |
| 已过期 | 红 | 重新预约 |
"状态机 + 操作集"映射(系列记录页反复强调)——App 23 只有"待使用/已完成"2 状态 + 无操作按钮(只有点击 Toast)——"最简记录页"。
真实预约记录的操作:
- 待使用 →取消(发短信/推送给管理员)
- 使用中 →签到(确认到场)
- 已完成 →评价(设备状态反馈)
"记录 = 状态机 + 操作"——读者做预约类 App 时,"记录页"要按状态机设计操作。
九、App 23 与 App 14 场地预约的对比
App 23 实验室预约与 App 14 运动场地预约是同类"预约应用"——对比:
| 维度 | App 14 场地预约 | App 23 实验室预约 |
|---|---|---|
| 主题色 | 绿 #16A34A | 深青 #0F766E |
| 预约对象 | 场地(篮球场/足球场) | 设备(显微镜/示波器) |
| 首页 | 5 块(Hero/入口/周图/场地/排行) | 3 块(Chip/Grid) |
| 时段选择 | 2 列 Grid 三态(可选/已选/已满) | Flex 换行单选(4 时段) |
| 详情页 | 无独立详情 | 4 卡详情(身份/参数/说明/预约) |
| 状态 | 空闲/已约满 | 空闲/使用中/维护中 |
| 个人中心 | 6 块(含签到) | 4 块最简 |
两类预约 App 的差异根源:
- App 14 运动:内容型+行动型混合——首页要营销(推荐场地)、个人中心要留存(签到)
- App 23 实验室:纯任务型——首页只要列表、个人中心只要统计
"预约对象决定 App 形态"——场地预约偏"消费"(休闲)、设备预约偏"工作"(科研)——"消费型预约丰富、工作型预约极简"。
十、"预约提交"的完整校验链
App 23 的 SubmitBtn 无校验(默认选中时段)——真实预约提交应有完整校验链:
1. 设备状态校验——"这台设备现在可预约吗"(空闲才可约、维护中拦截):
if (device.status === '维护中') { toast('设备维护中,暂不可预约'); return; }2. 时段冲突校验——"这个时段有人约了吗"(前端预检 + 后端最终校验):
if (isSlotBooked(deviceId, date, slot)) { toast('该时段已被预约,请选择其他时段'); return; }3. 用户资格校验——"用户有权限预约这台设备吗"(特殊设备需导师授权)。
4. 预约数量校验——"用户同时段预约上限"(防刷)。
"设备状态 + 时段冲突 + 用户资格 + 数量上限"4 层校验——真实预约系统的完整防线——App 23 默认选中免校验是"默认值设计"的简化,真实项目"后端校验"不可少。
十一、"预约记录"的降序与分页
App 23 的预约记录是 4 条硬编码——真实预约记录需要降序 + 分页:
1. 按日期降序:
// 真实项目:最新预约在前 const sorted = bookings.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt) );2. 分页加载:
// 上滑加载更多 Scroll() { List() { ... } .onReachEnd(() => { this.page++; this.loadMoreBookings(); }) }3. 状态分组(进阶):按"待使用/已完成/已取消"分组显示,或 Tab 筛选。
"降序 + 分页 + 分组"是预约记录页的 3 个真实需求——App 23 只有 4 条硬编码(无排序/分页)——"记录列表的数据管理"是真实项目的必修课。
十二、"预约成功"的后续流程
App 23 点击"确认预约"只弹 Toast——真实预约成功后有完整后续流程:
1. 生成预约凭证——"预约单号 + 二维码"(到实验室扫码核销)。
2. 通知相关方——预约人(提醒)、实验室管理员(审批/准备)。
3. 状态流转——预约 → 待使用(记录列表出现)→ 使用 → 完成。
4. 积分/信用(可选)——按时履约 +信用、放鸽子 -信用(科研预约的信用体系)。
"预约成功 ≠ 结束"——后续的"凭证 + 通知 + 流转 + 信用"才是完整预约系统——App 23 的"预约成功"是入口,真实系统是"预约闭环"。
十三、"Flex 换行" vs "Grid" 的时段选择器
App 23 的 SlotCard 用Flex({ wrap: FlexWrap.Wrap })换行布局——对比系列其他"选择器"的布局:
| App | 选择器 | 布局 | 数量 |
|---|---|---|---|
| App 18 兴趣 | 10 标签多选 | Flex 换行 | 10 个 |
| App 19 分类 | 8 分类多选 | Flex 换行 | 8 个 |
| App 21 题型 | 4 单选 | Row 一行 | 4 个 |
| App 22 时段 | 4 时段单选 | Flex 换行 | 4 个 |
| App 23 时段 | 4 时段单选 | Flex 换行 | 4 个 |
| App 16 商品 | 6 商品单选 | Grid 3 列 | 6 个 |
"Flex 换行 vs Grid"的选择:
- Flex 换行(App 23):标签长短不一、数量 4-10 → 自动换行,宽度自适应内容
- Grid(App 16):卡片等宽、需要整齐网格 → 固定列数,宽度均分
"时段选择器用 Flex 换行"是合理选择——时段标签长度固定("09:00-11:00"9 字符),Flex 换行排成 2-3 行整齐胶囊——"标签类选择器用 Flex、卡片类选择器用 Grid"。
App 22 和 App 23 的"时段选择"都用 Flex 换行——"时段胶囊 = Flex 换行"是时段选择的稳定模式(对比 App 14 场次 Grid 三态是"复杂时段")。
十四、"预约记录"的"设备 + 时间"双信息
App 23 预约记录卡是"设备 + 时间 + 状态"3 信息——"预约记录必含设备和时间":
记录卡的信息架构:
- 设备 emoji(28sp)——"约的什么"
- 日期 + 时段(14sp)——"什么时候用"
- 状态(12sp 状态色)——"现在什么状态"
"设备 + 时间 + 状态"3 要素是预约记录的标配——用户扫一眼知道"昨天约了显微镜 14:00-16:00,已完成"。
对比系列其他记录卡:
- App 14 场地记录:场地 + 时间 + 状态
- App 15 分类记录:物品 + 分类 + 状态
- App 16 兑换记录:商品 + 积分 + 状态
- App 23 预约记录:设备 + 时间 + 状态
"记录卡 = 业务对象 + 时间 + 状态"——3 要素是所有"记录列表"的通用信息架构——读者做任何"记录"(预约/订单/日志)都按这 3 要素设计。
十五、总结
App 23 预约页解析完毕。4 时段 Flex 真单选(@State slot)+ 提交动态回显 + 预约记录 2 状态是核心组件。"默认选中第一时段 = 免校验"是默认值设计的好处。"预约 4 层校验"、"记录降序分页"、"预约成功后续流程"、"Flex vs Grid 选择器"、"记录 3 要素"是真实预约系统的进阶能力。"找 → 看 → 选 → 记"4 步预约闭环是预约类 App 的标准流程。