鸿蒙原生应用 HarmonyOS 6.0实战:拆解 —— 设备详情页的参数表与近期预约时段卡
App 23「实验室设备预约」设备 Tab(Func1Tab),是设备详情页——4 卡结构:InfoCard(🔬 120 高图标块 + 高倍显微镜 + 空闲标签 + 位置)+ SpecCard(设备参数表:型号/倍率/光源/模式)+ GuideCard(使用说明 4 条)+ BookingList(近期预约 2 时段 + "预约该时段"按钮)。本篇基于
23-lab-booking/entry/src/main/ets/pages/Func1Tab.ets(约 145 行)逐段拆解,附 4 张实机截图。
一、整体结构:4 卡详情页
Func1Tab 是**"设备详情"的 4 卡布局**——信息/参数/说明/预约 4 层:
build() { Column() { this.Header() Scroll() { Column({ space: 16 }) { this.InfoCard() this.SpecCard() this.GuideCard() 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) }4 个块按"是什么 → 参数 → 怎么用 → 怎么约"漏斗展开:
- InfoCard— 设备身份(大图标 + 名称 + 状态 + 位置)
- SpecCard— 设备参数(型号/倍率/光源/模式)
- GuideCard— 使用说明(4 条操作指引)
- BookingList— 近期预约(2 时段 + 预约按钮)
"详情页 4 卡"是设备/商品详情页的通用结构(电商商品详情 = 图片 + 参数 + 详情 + 购买;设备详情 = 图标 + 参数 + 说明 + 预约)——"详情页 = 身份 + 参数 + 指南 + 行动"。
项目源码开源:https://gitee.com/codenestFlow/HarmonyOSHub
二、Header + InfoCard:设备身份卡
Header 是"设备详情"单行(系列标准)。
InfoCard是设备身份卡——大图标 + 名称 + 状态 + 位置:
@Builder InfoCard() { Column({ space: 12 }) { Row() { Text('🔬').fontSize(48) } .width('100%').height(120) .backgroundColor(C.primarySoft).borderRadius(D.rLg) .justifyContent(FlexAlign.Center) Text('高倍显微镜').fontSize(18).fontWeight(FontWeight.Bold).fontColor(C.text).width('100%') Row({ space: 8 }) { Text('空闲').fontSize(12).fontColor('#FFFFFF') .padding({ left: 8, right: 8, top: 3, bottom: 3 }) .backgroundColor(C.ok).borderRadius(4) Text('位于 B 楼 302').fontSize(12).fontColor(C.textDim) } .width('100%') } .width('100%') .padding(16) .backgroundColor(C.card) .borderRadius(D.rLg) .border({ width: 1, color: C.stroke }) }4 元素:
- 120 高 emoji 图标块(
C.primarySoft浅青底 + 🔬 48sp)——全宽大图标(120vp 高是全页最高图标块) - 设备名称(18sp 加粗)
- 空闲标签(白字绿底 +
C.ok)+位置("位于 B 楼 302"灰字)——状态 + 位置双信息
"空闲标签"用C.ok绿底白字——与首页状态色一致(绿=空闲)——"跨页状态色统一"。
"位于 B 楼 302"——设备位置(实验室物理位置)——"设备详情必须含位置"(用户要去实体实验室用设备)。
三、SpecCard:设备参数表
SpecCard 是"标签 + 值"参数表——4 行参数:
@Builder SpecCard() { Column({ space: 10 }) { Text('设备参数').fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text).width('100%') Row({ space: 8 }) { Text('型号').fontSize(13).fontColor(C.textDim).width(60) Text('Olympus BX53').fontSize(13).fontColor(C.text).layoutWeight(1) } .width('100%') Row({ space: 8 }) { Text('倍率').fontSize(13).fontColor(C.textDim).width(60) Text('40x - 1000x').fontSize(13).fontColor(C.text).layoutWeight(1) } .width('100%') Row({ space: 8 }) { Text('光源').fontSize(13).fontColor(C.textDim).width(60) Text('LED 冷光源').fontSize(13).fontColor(C.text).layoutWeight(1) } .width('100%') Row({ space: 8 }) { Text('模式').fontSize(13).fontColor(C.textDim).width(60) Text('明场 / 偏光 / 荧光').fontSize(13).fontColor(C.text).layoutWeight(1) } .width('100%') } .width('100%') .padding(16) .backgroundColor(C.card) .borderRadius(D.rLg) .border({ width: 1, color: C.stroke }) }4 行"标签 + 值":
| 标签 | 值 |
|---|---|
| 型号 | Olympus BX53 |
| 倍率 | 40x - 1000x |
| 光源 | LED 冷光源 |
| 模式 | 明场 / 偏光 / 荧光 |
"标签 60vp 固定宽 + 值 layoutWeight"——4 行标签右对齐、值左对齐——"参数表的标准布局"(标签列固定宽保证对齐)。
参数是"真实显微镜参数"——Olympus BX53 是真实显微镜型号(奥林巴斯生物显微镜)、40x-1000x 是标准倍率、明场/偏光/荧光是显微镜 3 种模式——"参数真实性 = 详情可信度"。
参数表的"键值对"模式——Row(标签 + 值)重复 4 次——真实项目应改为"数据驱动":
private specs: SpecItem[] = [ { label: '型号', value: 'Olympus BX53' }, { label: '倍率', value: '40x - 1000x' }, ... ]; // ForEach 渲染"参数表数据化"——加参数只需往数组加一项,不用复制粘贴 Row——"4 行参数"用数据驱动是更好的工程实践(本页手写 4 个 Row 是 demo 简化)。
四、GuideCard:使用说明
GuideCard 是4 条使用说明(操作指引):
@Builder GuideCard() { Column({ space: 10 }) { Text('使用说明').fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text).width('100%') Text('1. 预约后请在指定时间到达实验室').fontSize(13).fontColor(C.textSub).width('100%') Text('2. 使用前检查设备状态并登记').fontSize(13).fontColor(C.textSub).width('100%') Text('3. 使用后清洁镜头并归位').fontSize(13).fontColor(C.textSub).width('100%') Text('4. 如有故障请联系实验室管理员').fontSize(13).fontColor(C.textSub).width('100%') } .width('100%') .padding(16) .backgroundColor(C.card) .borderRadius(D.rLg) .border({ width: 1, color: C.stroke }) }4 条说明的"操作流程":
- 预约后准时到达(预约 → 到达)
- 使用前检查登记(检查 → 登记)
- 使用后清洁归位(清洁 → 归位)
- 故障联系管理员(异常 → 求助)
"编号 + 说明"(1./2./3./4.)——"编号让说明有序"——**用户按编号执行"预约→到达→检查→使用→清洁→归位→求助"**完整流程。
"实验室安全规范"——清洁归位/故障上报是实验室礼仪——"使用说明 = 实验室规章制度"——真实项目应加"安全须知"(戴手套/护目镜等)。
五、BookingList:近期预约 + 预约按钮
BookingList 是2 时段预约 + "预约该时段"按钮:
@Builder BookingList() { Column({ space: 12 }) { Text('近期预约').fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text).width('100%') Row({ space: 8 }) { Text('今天 14:00-16:00').fontSize(13).fontColor(C.text).layoutWeight(1) Text('已被预约').fontSize(11).fontColor(C.warn) } .width('100%').padding(10) .backgroundColor(C.cardSoft).borderRadius(D.rSm) Row({ space: 8 }) { Text('明天 09:00-11:00').fontSize(13).fontColor(C.text).layoutWeight(1) Text('空闲').fontSize(11).fontColor(C.ok) } .width('100%').padding(10) .backgroundColor(C.primarySoft).borderRadius(D.rSm) Button('预约该时段') .width('100%').height(44) .fontSize(14).fontColor('#FFFFFF').backgroundColor(C.primary) .borderRadius(D.rMd) .onClick(() => { promptAction.showToast({ message: '预约成功' }); }) } .width('100%') .padding(16) .backgroundColor(C.card) .borderRadius(D.rLg) .border({ width: 1, color: C.stroke }) }2 时段 + 状态:
- 今天 14:00-16:00(已被预约,橙)——
C.cardSoft浅灰底 - 明天 09:00-11:00(空闲,绿)——
C.primarySoft浅青底(可预约的时段用品牌浅色高亮)
"可预约时段用浅青底、已被约时段用浅灰底"——"可预约性通过底色区分"(比只靠状态字更直观)。
"预约该时段"按钮——44vp 深青底白字——点击弹 Toast "预约成功"——"预约 = 一键完成"。
注意:demo 未绑定"哪个时段可预约"——按钮无论"今天已被约"还是"明天空闲"都显示"预约该时段"——真实项目应"选中空闲时段 → 按钮变'预约 09:00-11:00'"(与 App 14 场地预约的动态按钮同款)。
六、@State 数量 = 0
Func1Tab0 个 @State——所有内容硬编码(高倍显微镜/Olympus BX53/使用说明/近期预约)——"纯展示详情页"。
真实项目应数据化:设备信息来自首页点击的设备(@Prop device或路由传参)——"首页点设备 → 详情页显示该设备"——本页硬编码"高倍显微镜"是 demo 简化(首页 6 台设备都能点,但详情页永远显示显微镜)。
七、"详情页 4 卡"模板的可复用性
App 23 设备详情页的"4 卡"是详情页通用模板:
InfoCard(身份:大图 + 名称 + 状态 + 位置) + SpecCard(参数表:标签 + 值) + GuideCard(说明:编号 + 指引) + BookingList(行动:时段 + 预约按钮)"详情页 4 卡"可复用到:
- 商品详情(图片 + 参数 + 详情 + 购买)
- 场馆详情(封面 + 设施 + 规则 + 预订)
- 课程详情(封面 + 大纲 + 须知 + 报名)
- 房间详情(图片 + 配置 + 须知 + 预订)
"身份 + 参数 + 指南 + 行动"4 层是详情页的通用信息架构——读者做任何"详情页"都可参考。
八、"参数表"的 3 种信息展示方式
App 23 的 SpecCard 用"标签 + 值"键值对表——真实设备/商品详情页的"参数展示"有 3 种方式:
1. 键值对表(App 23 当前):
型号 | Olympus BX53 倍率 | 40x - 1000x——适合"参数数量中等(4-8 项)",一列一值,清晰易读。
2. 分组表(真实产品):
【光学参数】倍率 40x-1000x / 物镜 4x/10x/40x 【电学参数】电压 220V / 功率 100W——适合"参数多(10+)",分组避免堆砌。
3. 卡片式(规格说明):
🔬 高倍显微镜 1000x 偏光 · LED冷光源 · 明场/偏光/荧光——适合"参数少(2-3 项)",一行概括。
"参数数量决定展示方式"——4-8 项用键值对、10+ 用分组、3 项内用卡片。
真实设备的"参数表"数据源:
// 设备参数从后端 API 拉取 interface DeviceSpec { label: string; value: string; } private specs: DeviceSpec[] = await getDeviceSpecs(deviceId);"参数表数据化"——参数从数据源来,不是硬编码——"详情页的参数是设备的元数据"(同一设备类型的设备共享参数模板)。
九、"使用说明"的 4 种场景应用
App 23 的 GuideCard 是"使用说明 4 条"——"说明/须知"类内容在 App 中的 4 种场景:
1. 操作步骤(App 23)——"预约→到达→检查→使用→清洁→归位"——编号有序,用户按序执行。
2. 安全须知(实验室进阶)——"戴护目镜""禁饮食""紧急开关位置"——警示性内容,用红色/黄色强调。
3. 常见问题 FAQ——"如何预约?""设备坏了找谁?"——问答形式,可展开/折叠。
4. 免责声明——"使用者自行承担损坏责任"——法律文本,小字灰底。
"说明内容决定 UI 形态":
- 操作步骤 → 编号列表(App 23)
- 安全须知 → 警示色卡片
- FAQ → 折叠面板
- 声明 → 小字灰底
"使用说明"是设备/商品详情页的标配——"说明清晰 = 减少客服压力"(用户看完说明能自己操作,不用问管理员)。
十、"预约时段"的冲突检测
App 23 的 BookingList 展示"今天已被预约/明天空闲"——真实项目的"预约时段冲突检测":
// 时段冲突检测 function isSlotBooked(deviceId: number, date: string, slot: string): boolean { return bookings.some(b => b.deviceId === deviceId && b.date === date && b.slot === slot ); }"预约冲突"的 3 层处理:
- 前端禁用(当前实现):已被约的时段置灰/显示"已被预约"
- 前端提示:用户点"已被约"时段 → 弹 Toast "该时段已被预约"
- 后端校验(最终防线):提交时服务端验证"无冲突"(防止并发抢同一时段)
"冲突检测"是预约系统的核心——"并发预约"是最难处理的场景(两人同时约同一时段)——后端乐观锁/数据库唯一约束是解决方案。
App 23 的简化:时段状态是硬编码("今天已被预约"写死)——真实项目从"预约表"实时计算——"时段状态 = 预约记录的聚合"。
十一、"设备详情"的路由传参设计
App 23 设备详情页硬编码"高倍显微镜"——真实项目应"首页点设备 → 详情页显示该设备"——路由传参是核心机制:
方案 1:路由参数:
// 首页点击设备 → 跳转详情页 router.pushUrl({ url: 'pages/DeviceDetail', params: { deviceId: d.id } }); // 详情页接收 onPageLoad() { const params = router.getParams() as Record<string, number>; this.deviceId = params.deviceId; this.loadDevice(this.deviceId); }方案 2:AppStorage 全局:
// 首页 AppStorage.setOrCreate('currentDevice', d.id); // 详情页 @StorageProp('currentDevice') deviceId: number;方案 3:@Prop 子组件(Tab 结构内传递):
// 首页 Tab 与设备 Tab 是平级,用 AppStorage 或路由最合适"路由传参"让详情页数据化——首页 6 台设备点击都进详情页,显示对应设备——**"详情页从硬编码变为动态"**是 demo 到产品的关键改进。
十二、"使用说明"的版本管理与审核
App 23 的 GuideCard 是静态 4 条说明——真实实验室的"使用说明"需要版本管理:
"使用说明"的 3 层管理:
- 版本——设备操作流程更新(如新操作规程)→ 说明 v1.2 替换 v1.1
- 审核——说明变更需管理员审核(安全规范不能随便改)
- 追溯——"操作事故"时能查"当时用的是哪个版本的说明"
"使用说明 = 设备文档"——真实项目应:
interface GuideVersion { version: string; content: string[]; effectiveDate: string; approvedBy: string; }"说明版本化"是设备管理(尤其医疗/科研)的合规要求——操作事故追责时需要"当时版本"。
App 23 的简化:说明硬编码 4 条——读者做实验室/医疗设备管理时,说明应支持"版本 + 审核 + 追溯"。
十三、"设备状态"与"详情页状态色"的一致性
App 23 详情页的"空闲"标签(绿底白字)与首页设备卡的"空闲"状态色(C.ok绿字)颜色语义一致:
首页设备卡:Text(d.status).fontColor(d.statusColor)(绿字)详情页标签:Text('空闲').backgroundColor(C.ok)(绿底白字)
"同状态同颜色、不同形态":
- 首页:状态色文字(列表紧凑,文字色省空间)
- 详情页:状态色标签(详情宽松,色块更醒目)
"状态色跨页一致"是设计系统原则——用户在首页看到"绿色 = 空闲",到详情页看到绿色标签自动理解"空闲"——"颜色语义全局统一"。
"色字 vs 色块"的选择:
- 列表卡(信息多):状态用"色字"(不占空间)
- 详情卡(信息少):状态用"色块标签"(醒目)
"状态展示形态 = 页面空间"——读者做自己的设计系统时,"同状态同色、形态随空间"是标准。
十四、"详情页"的返回与导航
App 23 详情页是 Tab 内页面(Func1Tab),无返回按钮——真实项目的"详情页"有返回导航:
"页面层级"导航:
首页(Tab)→ 点击设备 → 设备详情(push)→ 返回ArkUI 页面导航:
// push 进入详情页 router.pushUrl({ url: 'pages/DeviceDetail', params: { id: 1 } }); // 详情页返回 router.back();"详情页 3 要素"(导航相关):
- 返回按钮(左上角"←")——回到列表
- 面包屑/标题——"设备详情"标题
- 底部操作——"预约该时段"(App 23 的"预约"按钮是详情页的"行动锚点")
App 23 的简化:详情页是 Tab 内页面(无 push/back)——真实项目"详情页"通常独立路由——"Tab 详情 vs 路由详情"是两种导航模式(Tab 详情简单、路由详情灵活)。
十五、"预约按钮"的动态化与状态联动
App 23 详情页底部的"预约"按钮是静态按钮——无论设备状态如何,按钮始终可点击。真实项目中,预约按钮应与设备状态联动:
"按钮状态机"设计:
- 空闲→ 按钮可点击,文案"立即预约",主色背景
- 使用中→ 按钮禁用,文案"使用中",灰色背景
- 维护中→ 按钮禁用,文案"维护中",橙色背景
真实项目的动态按钮:
// 按钮状态跟随设备状态 Button(this.getBtnText(device.status)) .enabled(device.status === '空闲') .backgroundColor(device.status === '空闲' ? C.primary : C.textDim) .onClick(() => { /* 跳转预约页 */ })"按钮 = 状态的可操作化"——按钮不仅是入口,更是状态的视觉反馈——**"状态 → 按钮 → 行动"**是详情页的核心交互链路。读者做设备/场地预约类 App 时,务必让按钮跟随状态变化,避免用户点击后才发现"不可预约"。
十六、总结
App 23 设备详情页解析完毕。4 卡结构(身份/参数/说明/预约)+ 大图标 + 参数表 + 可预约性底色区分是核心组件。**"详情页 4 卡 = 身份 + 参数 + 指南 + 行动"是详情页通用模板。"状态色跨页一致"、"路由传参"、"页面导航"、"按钮动态化"是真实项目的进阶能力。"参数表数据化"和"预约按钮动态化"**是具体改进方向。