基于鸿蒙OS开发静脉输液智能监控系统(17)-家属端远程监控UI设计
基于鸿蒙OS开发静脉输液智能监控系统(17)-家属端远程监控UI设计
目录
- 1. 家属远程监控场景
- 2. FamilyHomePage远程监控面板
- 3. FamilyAlertPage预警记录
- 4. 远程与本地的数据同步设计
- 5. 家属端信息脱敏
- 6. 情感化设计
- 7. 家属端交互流程
- 8. 数据加载与刷新
- 9. 家属端与患者端的差异对比
1. 家属远程监控场景
1.1 不在医院时的焦虑
在传统输液场景中,家属往往面临着一种独特的焦虑——他们不在患者身边,却时刻牵挂着亲人的输液状况。这种焦虑并非无端产生,而是由以下几个现实因素共同催生的:
信息真空的痛苦:当家属离开医院去处理工作、照顾孩子或休息时,他们对亲人的输液进度一无所知。电话联系患者往往得不到准确信息——患者可能正在睡觉,或者自己也说不清楚剩余液量。这种信息真空让家属始终处于一种悬而未决的心理状态中,无法安心做任何事情。
突发状况的恐惧:输液过程中可能出现的异常——回血、输液管折叠、滴速异常、药液即将输完——这些状况如果没有被及时发现和处理,可能造成严重后果。家属不在身边,无法第一时间发现异常,这种无力感是焦虑的核心来源。
时间估算的困难:家属需要估算何时返回医院比较合适。回来太早浪费等待时间,回来太晚可能错过输液结束的时间点。没有准确的预计完成时间,家属只能凭经验估算,而经验往往不可靠——不同药品的滴速不同,不同患者的耐受程度不同,中途可能调整滴速,这些变量使得粗略估算几乎毫无意义。
反复确认的疲劳:为了缓解焦虑,家属可能会频繁打电话询问护士或患者本人。然而医院病房的护士工作繁忙,可能无法及时接听电话;患者如果正在休息,反复的电话反而造成困扰。这种反复确认不仅疲劳,还可能引发医患关系紧张。
IVGuard 家属端的设计,正是为了解决这些痛点而生。通过实时、准确、可视化的输液状态展示,让不在医院的家属也能"看到"亲人的输液情况,从信息真空走向信息透明,从被动焦虑走向主动掌控。
1.2 关键信息一览需求
家属对输液信息的需求与患者端既有重叠又有差异。重叠的部分是核心状态信息,差异的部分是操作权限和信息深度。经过用户调研和场景分析,我们提炼出家属端最需要一目了然的关键信息:
液位百分比:这是家属最关心的数据——药液还剩多少?通过环形进度条直观展示,不需要理解毫升数等专业数据,一个"还剩65%"的视觉呈现就能让家属瞬间安心。液位信息必须准确、实时,这是家属端信任度的基石。
当前状态:输液进行中还是已暂停?是正常输液还是出现了异常?状态指示灯以红黄绿三色直观传达,不需要阅读文字就能理解当前情况。绿色代表一切正常可以安心,黄色代表需要关注,红色代表需要立即行动——这种颜色语言是跨文化、跨年龄的通用语言。
预计剩余时间:基于当前液位和流速计算出的预计完成时间,是家属安排自己行程的重要依据。"大约还有45分钟输完"这样的信息,让家属可以合理规划返回医院的时间,既不会过早回来空等,也不会过晚回来错过。
药品名称:知道正在输的是什么药,家属才能心中有数。虽然家属不需要了解药品的详细药理信息,但知道"正在输头孢"比"正在输某种药"更能让家属感到掌控感。同时,药品名称也是家属与医生沟通时的重要信息。
流速信息:流速的展示让家属能够判断输液是否正常。如果平时流速是2.0 ml/min突然变成了0.5 ml/min,即使状态灯还是绿色,家属也能注意到这个变化并主动询问。流速数据赋予了家属更精细的感知能力。
以上信息的展示遵循"一屏可见"原则——所有关键信息在进入家属端首页时即可全部看到,不需要滚动、不需要点击展开、不需要切换Tab。这是对"关键信息一览"需求的最高级别满足。
1.3 快速联系护士需求
当家属观察到异常状态或心中有疑虑时,最迫切的需求是迅速联系到负责的护士。这个需求的紧迫性随状态严重程度而递增:
常规咨询:黄色预警状态下,家属可能想了解"为什么液位下降变慢了"或者"这个流速是否正常"。这类咨询不需要秒级响应,但需要在几分钟内得到回复。入口应该明显但不抢眼,让家属在需要时能快速找到。
紧急求助:红色预警状态下,家属的诉求是"我要立刻通知护士来处理"。这种情况下,联系护士的按钮必须是页面上最醒目、最容易点击的元素。理想状态下,从看到红色预警到拨通护士电话,操作步骤不超过两步。
联系护士的设计原则:
- 可见性:按钮始终可见,不需要滚动或搜索
- 可达性:按钮足够大,触控区域不小于48vp x 48vp
- 低门槛:一键操作,不需要填写表单或选择选项
- 反馈性:点击后有明确的反馈(跳转、拨号、消息发送确认)
在当前MVP版本中,"联系护士"按钮跳转到HospitalNavPage(护士站导航页面),家属可以看到护士站的位置信息。未来版本将实现直接拨号、即时消息、视频通话等更丰富的联系渠道。
1.4 预警通知需求
预警通知是家属端的生命线——当患者端检测到异常并触发预警时,家属必须第一时间收到通知。这个需求可以从通知的及时性、准确性和分级性三个维度来分析:
及时性:预警通知的延迟直接关系到患者安全。根据我们的设计目标,状态更新在30秒内到达家属端,而预警推送需要在5秒内到达。这意味着当患者端检测到液位低于20%触发红色预警时,家属端最多5秒后就应该收到通知。这5秒的延迟容忍度考虑了数据采集、处理、传输的全链路时间。
准确性:通知内容必须准确,不能出现误报或漏报。误报(如液位正常却发出红色预警)会严重损害家属对系统的信任,导致"狼来了"效应——当真正的预警到来时家属可能不再重视。漏报(如液位已经很低却没有通知)则直接威胁患者安全。因此,预警阈值的设计需要经过充分测试和校准。
分级性:不同严重程度的预警需要不同的通知方式:
- 黄色预警(注意):应用内通知,不震动,不打断当前操作
- 红色预警(紧急):系统级通知,震动,发出提示音,即使应用在后台也能引起注意
预警通知还需要避免过度打扰。如果某个输液过程频繁触发黄色预警(如液位在20%附近波动),系统应该进行去重处理,而不是在短时间内反复推送相同级别的预警。这种"预警疲劳"问题在医疗监控系统中尤为重要,过度预警比不预警更危险,因为它会让接收者对真正的紧急情况产生麻痹。
家属端的预警通知设计,在保证及时性的同时,还需要考虑医院环境——病房区域可能要求手机静音,所以震动提醒和视觉高亮可能比声音提醒更实用。这些都是我们在UI设计中需要权衡的因素。
2. FamilyHomePage远程监控面板(169行代码)
FamilyHomePage 是家属端的核心页面,承担着"一眼看懂亲人输液状态"的设计使命。整个页面约169行代码,紧凑而高效,在有限的代码量中实现了完整的信息展示和交互功能。以下逐模块进行深入解析。

2.1 患者信息卡片
患者信息卡片是FamilyHomePage最核心的UI模块,它将所有关键信息集中在一个视觉单元内,实现"一屏可见"的设计目标。
``typescript
Column({ space: 12 }) {
Text(${this.patientName} | 床号${this.bedNumber}`)
.fontSize(16).fontWeight(FontWeight.Medium)
Row() {
Text('状态指示: ').fontSize(14)
Row()
.width(16).height(16).borderRadius(8)
.backgroundColor(this.alertStatus === ‘green’ ? ‘#4CAF50’ :
this.alertStatus === ‘yellow’ ? ‘#FF9800’ : ‘#F44336’)
}
LevelProgress({ level: this.currentLevel })
Text(${this.medicineName}).fontSize(14).fontColor(‘#666666’)
Text(流速: ${this.flowRate.toFixed(1}` ml/min).fontSize(13).fontColor(‘#888888’)
}
``
代码结构分析:
这段代码使用了ArkUI的声明式布局语法,通过Column容器实现纵向排列,space: 12参数设置了子元素之间12vp的统一间距。这种统一间距的设计既保持了视觉的一致性,又避免了手动设置每个子元素的margin,减少了代码量。
第一层:患者标识:
患者姓名和床号是家属确认"这是我的亲人"的第一信息。使用模板字符串拼接,中间以竖线|分隔,这是信息展示中常见的分隔符约定——简洁、对称、易读。字号16vp保证了在手机屏幕上的可读性,FontWeight.Medium(相当于500字重)让关键信息在视觉上比普通文字更突出,但不像Bold那样过于厚重。
第二层:状态指示灯:
这部分使用了嵌套Row的方式——外层Row水平排列"状态指示"文字和彩色圆点,内层Row通过宽高16vp和borderRadius(8)(恰好是宽高的一半)形成圆形。颜色选择遵循Material Design色彩规范:
#4CAF50:Material Green 500,代表正常/安全#FF9800:Material Orange 500,代表注意/警告#F44336:Material Red 500,代表紧急/危险
三元运算符的链式使用简洁地实现了三种状态的颜色映射。当alertStatus既不是’green’也不是’yellow’时,默认使用红色,这是一种"安全优先"的设计——在状态不确定时,显示最严重的颜色,避免家属因看到绿色而放松警惕。
第三层:液位进度环:
LevelProgress组件以环形进度条的方式展示当前液位百分比。这是复用自患者端的组件,在家属端以只读模式展示,没有交互操作。环形进度条的视觉面积较大,是信息卡片中最醒目的元素,符合液位作为最核心信息的设计优先级。
第四层:药品信息:
药品名称使用14vp字号,颜色#666666(中灰色),视觉权重低于患者标识和状态指示,但仍然清晰可读。这种层级化的字体颜色使用,让信息在视觉上形成自然的优先级梯度——最重要的信息最醒目,辅助信息较淡但不消失。
第五层:流速数据:
流速信息使用更小的13vp字号和更淡的#888888灰色,因为它是最次要的辅助信息。 oFixed(1)确保流速始终保留一位小数,避免出现"2 ml/min"或"2.333333 ml/min"这类不规范的显示。流速单位"ml/min"使用国际通用的医学计量单位,专业且无歧义。
信息层级总结:
| 层级 | 信息 | 字号 | 字重/颜色 | 设计意图 |
|---|---|---|---|---|
| L1 | 患者姓名+床号 | 16vp | Medium | 确认身份,最优先 |
| L2 | 状态指示灯 | 14vp | 彩色圆点 | 快速判断状态 |
| L3 | 液位进度环 | - | 环形图 | 核心数据可视化 |
| L4 | 药品名称 | 14vp | #666666 | 辅助信息 |
| L5 | 流速数据 | 13vp | #888888 | 参考信息 |
2.2 状态指示灯设计
状态指示灯是家属端最直觉化的信息传达元素。一个16x16vp的彩色圆点,不需要阅读任何文字,家属就能瞬间理解当前输液状态。这种"直觉优于阅读"的设计理念在医疗场景中尤为重要——在紧张或焦虑的状态下,人的阅读理解能力会下降,但对颜色的直觉反应不会。
三种状态的详细定义:
绿色(正常,安全)— #4CAF50:
- 触发条件:液位 > 50%
- 语义:输液正常运行,一切在掌控之中
- 家属心理:安心,可以继续做其他事情
- 交互建议:无需任何操作,正常等待即可
绿色的选择不是随意的。#4CAF50是Material Design规范中定义的标准绿色,经过大量用户测试证明具有良好的辨识度和积极的心理暗示。与更亮的绿色(如#8BC34A)相比,#4CAF50更加沉稳,不会给人"刺眼"的感觉;与更深的绿色(如#388E3C)相比,它在浅色背景上的对比度更好。
黄色(注意,液位较低)— #FF9800:
- 触发条件:20% <= 液位 <= 50%
- 语义:输液正常进行,但液位已经较低,需要开始关注
- 家属心理:留意,可以准备联系护士了解情况
- 交互建议:可以联系护士询问,但不是紧急情况
黄色选择#FF9800而非更常见的#FFC107(Material Yellow),是因为在医疗场景中,黄色需要传达"注意"而非"警告"的语义。#FF9800的橙色调让它在"注意"和"警告"之间取得了平衡——它比纯黄色更显眼,提醒家属关注,但又不至于像红色那样引发紧迫感。
红色(紧急,需要关注)— #F44336:
- 触发条件:液位 < 20%
- 语义:液位已经很低,需要尽快处理
- 家属心理:立即行动,联系护士
- 交互建议:立即点击"联系护士"按钮
红色的选择同样经过考量。#F44336是Material Red 500,在白色背景上具有极高的辨识度。与更亮的#EF5350相比,它略深,在LCD屏幕上的显示效果更好;与更深的#D32F2F相比,它的饱和度更高,更容易引起视觉注意。
updateAlertStatus()逻辑解析:
typescript updateAlertStatus() { if (this.currentLevel > 50) { this.alertStatus = 'green' } else if (this.currentLevel >= 20) { this.alertStatus = 'yellow' } else { this.alertStatus = 'red' } }
这个方法的逻辑清晰而严谨:
- 液位超过50%,状态为绿色——安全区间
- 液位在20%-50%之间,状态为黄色——关注区间
- 液位低于20%,状态为红色——紧急区间
阈值的选择(50%和20%)并非随意:
- 50%作为绿色分界:当液位过半时,距离输液结束还有较长时间,家属无需关注。50%是一个整数心理锚点,便于理解和记忆。
- 20%作为红色分界:按照典型流速,20%液位大约还剩15-30分钟的输液时间,这是护士需要前来处理(拔针或换瓶)的提前量。低于20%触发紧急通知,给护士留出足够的响应时间。
值得注意的是,这个阈值设置是针对MVP版本的简化设计。在实际产品中,阈值应该是可配置的——不同药品、不同患者、不同科室可能有不同的阈值要求。例如,某些快速输液的药品可能需要在30%时就触发红色预警,而慢速输液可以放宽到15%。这种可配置性将在后续版本中实现。
状态转换的防抖处理:
在液位恰好处于阈值附近时(如50.1%到49.9%到50.2%),状态指示灯可能在绿色和黄色之间频繁切换,造成视觉闪烁。为避免这种情况,实际实现中应该加入滞后区间(hysteresis)——例如,从绿色变为黄色的阈值是50%,但从黄色变回绿色的阈值是55%。这样在阈值附近的波动不会导致状态频繁切换。当前MVP版本暂未实现这一优化,但这是后续版本必须处理的问题。
2.3 LevelProgress环形进度
LevelProgress组件在家属端复用自患者端,这是IVGuard项目中组件复用策略的一个典型案例。
``typescript
@Component
export struct LevelProgress {
@Prop level: number = 0
build() {
Stack() {
Progress({ value: this.level, total: 100, type: ProgressType.Ring })
.width(120)
.height(120)
.color(this.level > 50 ? ‘#4CAF50’ : this.level >= 20 ? ‘#FF9800’ : ‘#F44336’)
.backgroundColor(‘#E0E0E0’)
Text(`$`{Math.round(this.level)`}`%)
.fontSize(28)
.fontWeight(FontWeight.Bold)
}
}
}
``
家属端与患者端的复用差异:
| 特性 | 患者端使用 | 家属端使用 |
|---|---|---|
| 数据绑定 | @Link 双向绑定 | @Prop 单向绑定 |
| 交互操作 | 可能有动画效果 | 纯只读展示 |
| 点击事件 | 有(触发详细信息弹窗) | 无 |
| 动画控制 | 可手动暂停/恢复 | 无需控制 |
在家属端,LevelProgress通过@Prop接收液位数据,实现单向数据流——数据从DataStore流向组件,组件只负责展示,不反向修改数据。这符合家属端"只读监控"的定位。
环形进度条的视觉参数:
- 尺寸:120x120vp,在手机屏幕上足够醒目但不占据过多空间
- 颜色动态:进度环颜色随液位变化,与状态指示灯保持一致的红黄绿语义
- 背景环:
#E0E0E0(浅灰色),与白色页面背景有足够对比度但不突兀 - 中心文字:28vp粗体百分比数字,是整个卡片中字号最大的文字,突出液位的核心地位
Math.round()的使用确保百分比显示为整数,避免出现"64.7%"这类精确但视觉上不整洁的数字。在医疗场景中,液位传感器本身的精度通常在正负2%以内,显示小数位给人一种不真实的精确感,整数百分比更符合实际测量精度。
2.4 "联系护士"大按钮
"联系护士"按钮是家属端最重要的操作入口,它在视觉和交互设计上都遵循了"紧急操作的可达性"原则。
typescript Button('联系护士') .width('80%') .height(56) .backgroundColor('#FF9800') .borderRadius(28) .fontColor(Color.White) .fontSize(18) .fontWeight(FontWeight.Medium) .onClick(() => { this.navController.pushPath({ name: 'HospitalNavPage' }) })
设计决策详解:
宽度80%:不使用100%宽度是因为两侧留出边距(约10%每侧)使按钮在视觉上更加舒适。满宽按钮在移动端容易误触边缘,而80%宽度在保持醒目的同时提供了安全边距。
高度56vp:这是Material Design规范推荐的触摸目标最小高度,考虑到长辈用户可能手指不够灵活,56vp的高度提供了充足的触控面积。
圆角28vp:恰好是高度的一半,形成完全圆角(pill shape)的效果。圆角按钮比直角按钮显得更友好、更少攻击性,这与家属端"安抚而非恐吓"的情感化设计理念一致。
橙色背景#FF9800:按钮使用橙色而非绿色或蓝色,有以下考量:
- 橙色传达"行动号召"的语义——它不是"一切正常"的绿色,也不是"危险紧急"的红色,而是"你可以做点什么"的橙色
- 橙色在白色背景上足够醒目,但不给人压迫感
- 橙色与状态指示灯的黄色使用同色系,视觉上有关联性——黄色表示"注意",橙色按钮表示"注意后可以采取的行动"
字体白色:白色文字在橙色背景上的对比度符合WCAG 2.0 AA标准(对比度4.5:1以上),保证可读性。
onClick行为:当前版本跳转到HospitalNavPage,这是一个护士站导航页面,家属可以看到护士站的位置信息。这个跳转逻辑是MVP的简化实现,未来将扩展为:
typescript .onClick(() => { // 未来版本 AlertDialog.show({ title: '联系护士', message: '请选择联系方式', buttons: [ { text: '拨打电话', action: () => { this.callNurse() } }, { text: '发送消息', action: () => { this.sendMessage() } }, { text: '导航到护士站', action: () => { this.navController.pushPath({ name: 'HospitalNavPage' }) }} ] }) })
按钮在页面中的位置:联系护士按钮位于患者信息卡片下方,是页面的第二个主要视觉区域。它不在信息卡片内部(避免与展示信息混淆),也不在页面底部(避免需要滚动才能到达)。中间偏下的位置确保了:查看完信息后,视线自然下移就能看到操作按钮。
2.5 最近预警摘要
最近预警摘要模块为家属提供了快捷的预警信息浏览,无需跳转到FamilyAlertPage就能看到最近发生的预警。
``typescript
Column({ space: 8 }) {
Text(‘最近预警’).fontSize(15).fontWeight(FontWeight.Medium)
ForEach(this.recentAlerts.slice(0, 3), (alert: AlertInfo) => {
Row() {
Row()
.width(8).height(8).borderRadius(4)
.backgroundColor(alert.severity === ‘high’ ? ‘#F44336’ : ‘#FF9800’)
Text(alert.message).fontSize(13).fontColor('#333333')
.layoutWeight(1)
Text(this.formatTime(alert.timestamp)).fontSize(12).fontColor('#999999')
}
.width('100%')
.padding(12)
.borderRadius(8)
.backgroundColor('#FFF3E0')
})
}
``
slice(0, 3)的设计意图:
只显示最近3条预警而非全部,基于以下考量:
- 信息密度控制:首页是监控面板,不是记录列表。3条预警提供了足够的最近信息,同时不会让页面变得过长
- 认知负荷管理:人在紧张状态下处理信息的能力下降,3条是一个可以快速浏览的数量
- 查看更多的入口:如果家属需要查看完整预警记录,可以点击"预警记录"导航入口跳转到FamilyAlertPage
背景色#FFF3E0:
这是Material Design规范中的Orange 50色,是非常浅的橙色背景。选择这个颜色而非白色背景有双重考量:
- 视觉区分:预警摘要区域需要与普通信息区域有视觉区分,浅橙色背景在不使用边框的情况下实现了区域划分
- 情感暗示:橙色背景微妙地传达"这里的信息需要关注",但不至于像红色背景那样引发紧张感
预警条目的布局:
每条预警使用Row水平布局,包含三个元素:
- 8x8彩色圆点:与状态指示灯同样的红/橙颜色语义,让家属一眼判断预警严重程度
- 预警消息文本:使用layoutWeight(1)占据剩余空间,长文本自动截断
- 时间戳:右对齐,灰色小字,提供时间参考但不抢夺视觉注意力
时间格式化函数:
typescript formatTime(timestamp: number): string { const date = new Date(timestamp) const hours = date.getHours().toString().padStart(2, '0') const minutes = date.getMinutes().toString().padStart(2, '0') return `$`{hours`}`:`$`{minutes`}` }
时间格式化为"HH:mm"格式,只显示时分不显示秒,也不显示日期(因为最近预警都是当天的)。这种精简的时间展示避免了信息过载,同时保留了足够的时间参考价值。
空状态处理:
当recentAlerts为空时(整个输液过程没有触发预警),这个区域不应该显示空白或消失,而应该显示一个"暂无预警"的提示。这种空状态设计在UI/UX中被称为"空状态设计"(Empty State Design),它的作用是:
- 告诉用户"这里本来应该有内容,但目前没有"而非"这里出错了"
- 给用户安心感——"没有预警"本身就是一个好消息
typescript if (this.recentAlerts.length === 0) { Row() { Text('暂无预警,一切正常').fontSize(14).fontColor('#4CAF50') } .width('100%') .padding(16) .justifyContent(FlexAlign.Center) }
2.6 导航
FamilyHomePage底部提供两个导航入口,为家属提供跳转到其他功能页面的通道。
预警记录入口:
typescript Row() { Text('查看全部预警记录').fontSize(14).fontColor('#FF9800') Text(' >').fontSize(14).fontColor('#FF9800') } .onClick(() => { this.navController.pushPath({ name: 'FamilyAlertPage' }) })
使用橙色文字与"联系护士"按钮保持视觉一致性,传达"这是一个可以点击的操作"的暗示。右箭头">"是移动端常见的"查看更多"视觉约定,用户不需要学习就知道点击这里可以看到更多内容。
切换角色入口:
typescript Row() { Text('切换到患者端').fontSize(13).fontColor('#999999') } .onClick(() => { this.navController.replacePath({ name: 'PatientHomePage' }) })
切换角色入口使用灰色小字,视觉权重低于预警记录入口。这是因为角色切换是一个低频操作——家属通常不需要频繁切换到患者端。使用
eplacePath而非pushPath是因为切换角色意味着身份的变更,不应该保留"返回"的导航栈,避免用户在两个角色间来回跳转导致导航栈无限增长。
两个导航入口的视觉权重差异反映了一个设计原则:功能的使用频率应该影响其在界面中的视觉权重。高频功能(查看预警记录)更醒目,低频功能(切换角色)更低调。
3. FamilyAlertPage预警记录
FamilyAlertPage是家属端查看完整预警记录的页面,与FamilyHomePage上的"最近预警摘要"形成互补——摘要提供快速浏览,完整记录提供详细查看。

3.1 时间线列表设计
预警记录采用时间线(Timeline)列表设计,这是信息展示中成熟且广泛使用的模式。时间线设计有以下优势:
时间维度的直观性:输液过程中的预警天然具有时间顺序——先发生什么,后发生什么,间隔多久。时间线布局让这种时间关系一目了然,家属可以清楚地看到预警的时间分布和频率。
信息密度的平衡:与卡片列表相比,时间线在垂直方向上更紧凑,适合展示大量条目。与纯文本列表相比,时间线通过左侧的竖线和圆点提供了视觉结构,让信息更有层次感。
认知负担的低廉:时间线是人类最熟悉的信息组织方式之一——从历史教科书到聊天记录,人们已经形成了阅读时间线的习惯。不需要额外的学习成本。
``typescript
@Component
struct AlertTimelineItem {
@Prop alert: AlertInfo
private isLast: boolean = false
build() {
Row() {
Column() {
Row()
.width(12).height(12).borderRadius(6)
.backgroundColor(this.alert.severity === ‘high’ ? ‘#F44336’ : ‘#FF9800’)
if (!this.isLast) {
Row()
.width(2)
.layoutWeight(1)
.backgroundColor('#E0E0E0')
}
}
.width(20)
.margin({ left: 16, right: 12 })
Column({ space: 4 }) {
Text(this.formatAlertType(this.alert.type))
.fontSize(14)
.fontWeight(FontWeight.Medium)
Text(this.alert.message)
.fontSize(13)
.fontColor('#666666')
Text(this.formatFullTime(this.alert.timestamp))
.fontSize(12)
.fontColor('#999999')
}
.layoutWeight(1)
.padding({ top: 0, bottom: 16 })
}
.width('100%')
.alignItems(VerticalAlign.Top)
}
}
``
时间线结构解析:
每条预警记录分为左右两部分:
- 左侧:时间线结构元素(圆点+竖线),固定宽度20vp
- 右侧:预警详情文本,占据剩余宽度
左侧Column包含圆点和竖线。圆点12x12vp,颜色与严重程度关联。竖线2vp宽,浅灰色#E0E0E0,从圆点下方延伸到下一条记录的圆点上方。最后一条记录不显示竖线(通过isLast参数控制),避免出现悬挂的竖线。
右侧Column包含三层信息:
- 预警类型标签:14vp中等字重,如"液位预警"、“流速异常”
- 预警详细消息:13vp灰色,描述具体情况
- 完整时间戳:12vp浅灰色,精确到秒
3.2 预警类型标签
预警类型标签将预警按原因分类,帮助家属快速理解"为什么会有这个预警"。标签设计遵循"简洁+直观"原则:
typescript formatAlertType(type: string): string { const typeMap: Record<string, string> = { 'low_level': '液位偏低', 'flow_anomaly': '流速异常', 'infusion_complete': '输液完成', 'device_error': '设备异常', 'tube_blocked': '管路堵塞' } return typeMap[type] || '未知预警' }
类型标签使用中文而非英文代码或技术术语,因为家属不是医疗专业人员。"液位偏低"比"low_level"更容易理解,"管路堵塞"比"tube_blocked"更直观。这种用户友好的文案设计是家属端情感化设计的一部分——让技术信息变得人人可懂。
每种预警类型的详细语义解析:
液位偏低(low_level):这是最常见的预警类型,当输液瓶/袋中的药液液位低于预设阈值时触发。在当前阈值设置中,液位低于50%触发黄色预警,低于20%触发红色预警。家属看到"液位偏低"标签时,应该理解为"药液快要输完了,需要关注"。
流速异常(flow_anomaly):当输液流速偏离预设范围时触发。流速过快可能导致患者心脏负荷过大,流速过慢则可能意味着管路存在问题。流速异常的检测基于移动平均算法——计算最近5分钟的平均流速,与基线流速比较,偏差超过30%即触发预警。
输液完成(infusion_complete):当液位接近0%(低于2%)时触发,表示输液即将或已经完成。这不是一个异常预警,而是一个状态通知——通知护士前来拔针或更换输液瓶。对家属而言,这个标签意味着"输液快要结束了,护士会来处理"。
设备异常(device_error):当IVGuard硬件设备出现故障(如传感器失灵、通信中断、电量不足)时触发。设备异常需要护士进行现场检查和处理,家属看到这个标签时应该联系护士了解情况。
管路堵塞(tube_blocked):当检测到输液管路可能存在堵塞时触发。管路堵塞是输液过程中比较紧急的状况,需要护士立即处理,否则可能导致回血或其他并发症。这个标签应该以红色(high severity)展示。
3.3 严重程度彩色圆点
与FamilyHomePage的状态指示灯一致,FamilyAlertPage中的每条预警也使用彩色圆点标识严重程度。但两处的圆点尺寸不同:
- FamilyHomePage状态指示灯:16x16vp
- FamilyAlertPage预警圆点:12x12vp
这种尺寸差异是有意为之——首页的状态指示灯是"全局状态",需要更醒目;预警记录中的圆点是"单条记录标记",不应该过于抢眼,以免与预警文本争夺视觉注意力。
颜色映射也略有差异——预警记录只有红色(high)和橙色(normal)两种,没有绿色。这是因为预警记录列表中的条目都是"已经触发的预警",不存在"正常"状态。绿色只出现在首页的全局状态指示灯中,代表"当前无预警"。
严重程度的定义与判断标准:
| 严重程度 | 颜色 | 预警类型 | 判断标准 |
|---|---|---|---|
| high | #F44336 红色 | 液位 < 20%, 管路堵塞, 设备异常 | 需要立即行动 |
| normal | #FF9800 橙色 | 液位 20-50%, 流速偏差 | 需要关注 |
“normal"这个命名可能令人困惑——它不是"正常”,而是"常规预警",与"高危预警"(high)相对。在MVP版本中,我们选择了这种简化的两级分类,避免过多的级别增加用户认知负担。
3.4 时间排序:最新在前
typescript .sort((a: AlertInfo, b: AlertInfo) => b.timestamp - a.timestamp)
预警记录按时间戳降序排列,最新的预警在最上面。这种排序方式基于以下考量:
- 最新信息最重要:家属打开预警记录时,最想看到的是"最近发生了什么",而不是"最开始发生了什么"
- 减少滚动:最新信息在顶部意味着用户不需要滚动就能看到最重要的内容
- 与聊天记录的区别:现代聊天应用中,最新消息在底部,但需要滚动到底部才能看到。预警记录反过来——最新在顶部——是因为预警记录是"查看历史"而非"实时对话",用户的行为模式是"扫一眼最近发生了什么"然后离开
时间戳的数据类型:
AlertInfo中的timestamp字段使用number类型,存储的是Unix时间戳(毫秒级)。使用number而非Date对象是因为:JSON序列化/反序列化时Date对象会变成字符串,需要额外的转换;而number类型可以直接序列化,且支持直接的数学比较(排序)。
3.5 空状态设计
当没有任何预警记录时,FamilyAlertPage显示空状态:
``typescript
if (this.alerts.length === 0) {
Column() {
Text(‘暂无预警记录’)
.fontSize(16)
.fontColor(‘#999999’)
Text('输液过程一切正常,请安心等待')
.fontSize(14)
.fontColor('#BBBBBB')
.margin({ top: 8 })
}
.width(‘100%’)
.height(‘100%’)
.justifyContent(FlexAlign.Center)
}
``
空状态设计包含两层信息:
- 事实陈述:“暂无预警记录”——客观告知没有数据
- 情感安抚:“输液过程一切正常,请安心等待”——主观传达"这是好事"
第二层是空状态设计的精髓——不仅是"没有数据"的占位符,更是对用户情绪的积极引导。在家属端这个特殊的场景中,"没有预警"就是最好的消息,空状态设计应该将这个好消息明确传达出来。
空状态设计的心理学基础是"框架效应"(Framing Effect)——同一个事实,不同的表达方式会产生不同的心理效果。"暂无预警记录"是中性陈述,而"输液过程一切正常"是积极框架。将两者结合,既客观又暖心。
3.6 FamilyAlertPage完整代码结构
``typescript
@Entry
@Component
struct FamilyAlertPage {
@State alerts: AlertInfo[] = []
private navController: NavPathStack = new NavPathStack()
aboutToAppear() {
this.loadAlerts()
}
loadAlerts() {
const rawAlerts = DataStore.getAlerts()
this.alerts = rawAlerts.sort((a: AlertInfo, b: AlertInfo) => b.timestamp - a.timestamp)
}
build() {
NavNavigation(this.navController) {
Column() {
Text(‘预警记录’)
.fontSize(20)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 16 })
if (this.alerts.length === 0) {
this.EmptyView()
} else {
List({ space: 0 }) {
ForEach(this.alerts, (alert: AlertInfo, index: number) => {
ListItem() {
AlertTimelineItem({
alert: alert,
isLast: index === this.alerts.length - 1
})
}
})
}
.layoutWeight(1)
}
}
.width('100%')
.height('100%')
.padding(16)
}
}
@Builder EmptyView() {
Column() {
Text(‘暂无预警记录’).fontSize(16).fontColor(‘#999999’)
Text(‘输液过程一切正常,请安心等待’).fontSize(14).fontColor(‘#BBBBBB’)
.margin({ top: 8 })
}
.width(‘100%’)
.layoutWeight(1)
.justifyContent(FlexAlign.Center)
}
}
``
整个页面的代码结构简洁明了:aboutToAppear时加载预警数据,build方法根据数据是否存在渲染不同的UI。List组件使用ForEach遍历预警数组,每条预警渲染为一个AlertTimelineItem组件。layoutWeight(1)让List占据页面剩余空间,支持滚动浏览大量记录。
代码结构的可扩展性:
当前实现中,预警记录是一个简单的列表。未来版本可能需要扩展以下功能:
- 筛选功能:按预警类型、严重程度、时间范围筛选
- 搜索功能:在预警记录中搜索关键词
- 详情页:点击某条预警进入详情页,显示更完整的信息(如触发预警时的液位快照、流速趋势图等)
- 标记已读:区分已读和未读预警,新预警高亮显示
- 备注功能:家属可以为预警添加备注(如"已联系护士张三,15:30前来处理")
这些扩展功能不影响当前的基础架构,AlertInfo数据模型可以方便地增加字段,AlertTimelineItem组件可以增加交互元素(如点击事件、标记按钮等)。
4. 远程与本地的数据同步设计
数据同步是家属端面临的核心技术挑战。患者端采集数据、家属端展示数据,两端之间的数据一致性直接关系到监控的可靠性。本章从MVP方案到未来规划,逐层展开数据同步的设计思路。
4.1 MVP方案:同设备AppStorage共享
在MVP版本中,我们采用了最简方案——患者端和家属端运行在同一台设备上,通过AppStorage实现数据共享。
方案架构:
[患者端写入] -> AppStorage -> [家属端读取] | DataStore封装
AppStorage是ArkUI框架提供的应用级状态管理机制,它具有以下特性:
- 应用级作用域:整个应用共享同一个AppStorage实例,不同页面、不同组件都可以访问
- 响应式更新:当AppStorage中的值发生变化时,所有绑定了该值的UI组件都会自动刷新
- 持久化能力:配合PersistentStorage,AppStorage中的数据可以在应用重启后恢复
DataStore封装:
IVGuard项目对AppStorage进行了封装,提供了更高层的DataStore API:
``typescript
export class DataStore {
private static readonly PREFIX = ‘ivguard_’
static init() {
PersistentStorage.persistProp(‘ivguard_sessions’, ‘[]’)
PersistentStorage.persistProp(‘ivguard_medicines’, ‘[]’)
PersistentStorage.persistProp(‘ivguard_alerts’, ‘[]’)
PersistentStorage.persistProp(‘ivguard_current_level’, ‘100’)
PersistentStorage.persistProp(‘ivguard_flow_rate’, ‘2.0’)
PersistentStorage.persistProp(‘ivguard_patient_name’, ‘’)
PersistentStorage.persistProp(‘ivguard_bed_number’, ‘’)
}
static getSessions(): SessionInfo[] {
const raw = AppStorage.get(‘ivguard_sessions’) || ‘[]’
return JSON.parse(raw) as SessionInfo[]
}
static setSessions(sessions: SessionInfo[]) {
AppStorage.setOrCreate(‘ivguard_sessions’, JSON.stringify(sessions))
}
static getAlerts(): AlertInfo[] {
const raw = AppStorage.get(‘ivguard_alerts’) || ‘[]’
return JSON.parse(raw) as AlertInfo[]
}
static setAlerts(alerts: AlertInfo[]) {
AppStorage.setOrCreate(‘ivguard_alerts’, JSON.stringify(alerts))
}
static getCurrentLevel(): number {
return AppStorage.get(‘ivguard_current_level’) || 100
}
static setCurrentLevel(level: number) {
AppStorage.setOrCreate(‘ivguard_current_level’, level)
}
static getFlowRate(): number {
return AppStorage.get(‘ivguard_flow_rate’) || 2.0
}
static setFlowRate(rate: number) {
AppStorage.setOrCreate(‘ivguard_flow_rate’, rate)
}
static getPatientName(): string {
return AppStorage.get(‘ivguard_patient_name’) || ‘’
}
static setPatientName(name: string) {
AppStorage.setOrCreate(‘ivguard_patient_name’, name)
}
static getBedNumber(): string {
return AppStorage.get(‘ivguard_bed_number’) || ‘’
}
static setBedNumber(bed: string) {
AppStorage.setOrCreate(‘ivguard_bed_number’, bed)
}
}
``
Key命名规范:
所有Key使用ivguard_前缀,避免与AppStorage中其他应用或框架的Key冲突。Key名称使用小写下划线命名法(snake_case),与ArkUI社区惯例一致。
数据流方向:
在MVP方案中,数据流是单向的:
患者端(写入) -> DataStore -> 家属端(读取)
患者端是数据的唯一生产者,家属端是数据的消费者。这种单向数据流模型确保了数据一致性——不存在"两端同时修改同一份数据导致冲突"的问题,因为家属端只读不写。
MVP方案的局限性:
- 同设备限制:患者端和家属端必须运行在同一台设备上,无法实现真正的远程监控
- 角色切换模型:用户需要在患者端和家属端之间手动切换,不能同时运行
- 无推送能力:当应用在后台时,无法向家属端推送预警通知
- 无多用户支持:一个设备只能监控一个患者,无法同时监控多位亲人
这些局限性是MVP版本的设计取舍——在验证核心功能可用性之前,不值得投入大量工程资源解决分布式同步问题。但架构设计需要为未来的扩展预留空间。
AppStorage的数据序列化策略:
注意DataStore中对复杂数据类型(sessions、medicines、alerts)的处理方式——它们被JSON序列化为字符串后存储在AppStorage中,读取时再反序列化。这是因为AppStorage原生支持的类型有限(number、string、boolean等基本类型),不支持直接存储对象数组。
序列化策略的选择有以下考量:
- 简单性:JSON.stringify/parse是最通用的序列化方案,兼容性好
- 可读性:序列化后的字符串在调试时可以直接查看,便于排查问题
- 性能代价:每次读取都需要JSON.parse,对于频繁访问的数据可能有性能影响。但在MVP版本中,数据量小(通常只有几条session和alert),性能不是瓶颈
对于简单值(current_level、flow_rate等),直接存储number类型,不需要序列化。这种分类处理策略在简洁性和性能之间取得了平衡。
4.2 分布式数据同步预留
面向未来版本,我们设计了基于HarmonyOS分布式能力的跨设备数据同步方案。
DistributedData KV Store方案:
HarmonyOS提供了分布式键值数据库(Distributed KV Store),支持跨设备的数据自动同步。这是实现家属端远程监控的技术基础。
``typescript
import distributedKVStore from ‘@ohos.data.distributedKVStore’
class RemoteDataSync {
private kvManager: distributedKVStore.KVManager | null = null
private kvStore: distributedKVStore.SingleKVStore | null = null
async init() {
const kvManagerConfig: distributedKVStore.KVManagerConfig = {
bundleName: ‘com.ivguard.app’,
context: getContext(this)
}
this.kvManager = distributedKVStore.createKVManager(kvManagerConfig)
const kvStoreConfig: distributedKVStore.KVStoreConfig = {
storeId: 'ivguard_sync',
securityLevel: distributedKVStore.SecurityLevel.S2
}
this.kvStore = await this.kvManager.getKVStore(kvStoreConfig)
this.kvStore.on('dataChange', distributedKVStore.ChangeType.DATA_CHANGED,
(data: distributedKVStore.StoreChangeData) => {
this.handleRemoteUpdate(data)
})
}
async syncLevel(level: number) {
await this.kvStore.put(‘current_level’, level.toString())
}
async syncAlert(alert: AlertInfo) {
const key = ‘alert_’ + alert.timestamp.toString()
await this.kvStore.put(key, JSON.stringify(alert))
}
async syncSession(session: SessionInfo) {
await this.kvStore.put(‘patient_info’, JSON.stringify({
patientName: session.patientName,
bedNumber: session.bedNumber,
medicineName: session.medicineName
}))
}
handleRemoteUpdate(data: distributedKVStore.StoreChangeData) {
for (const entry of data.insertedEntries) {
this.processRemoteEntry(entry)
}
for (const entry of data.updatedEntries) {
this.processRemoteEntry(entry)
}
}
processRemoteEntry(entry: distributedKVStore.Entry) {
const key = entry.key as string
if (key === ‘current_level’) {
const level = parseFloat(entry.value.value as string)
AppStorage.setOrCreate(‘ivguard_current_level’, level)
} else if (key.startsWith(‘alert_’)) {
const alert = JSON.parse(entry.value.value as string) as AlertInfo
const existingAlerts = DataStore.getAlerts()
existingAlerts.push(alert)
DataStore.setAlerts(existingAlerts)
}
}
}
``
同步Key设计:
分布式KV Store的Key设计需要考虑同步效率和数据一致性:
| Key | 值类型 | 同步频率 | 说明 |
|---|---|---|---|
| current_level | string(number) | 每30秒 | 当前液位百分比 |
| flow_rate | string(number) | 每30秒 | 当前流速 |
| alert_{timestamp} | string(JSON) | 触发时 | 单条预警记录 |
| patient_info | string(JSON) | 会话开始时 | 患者基本信息 |
| session_status | string | 状态变更时 | 会话状态(运行/暂停/结束) |
Key设计的关键决策是将预警记录拆分为独立的Key(alert_{timestamp})而非存储为一个大的JSON数组。这样做的好处是:
- 增量同步:新增预警只需要同步一个Key,不需要同步整个预警数组
- 冲突减少:不同设备同时写入不同时间戳的预警不会产生冲突
- 同步效率:KV Store的同步粒度是Key级别,小Key比大Key同步更快
安全级别选择:
KV Store的安全级别选择了S2(Security Level 2),对应"隐私数据"级别。这个选择基于以下考量:
- S0(无安全要求):不适合,输液数据属于医疗健康数据
- S1(低安全级别):不适合,患者信息需要基本保护
- S2(隐私数据):合适,患者姓名、床号、药品名称属于个人隐私
- S3(敏感数据):过于严格,会限制设备间同步的灵活性
- S4(最高安全级别):不适合,我们的数据不涉及极度敏感的信息
冲突解决策略:
分布式环境下,数据冲突是不可避免的。例如,患者端和家属端可能同时读取并修改数据(虽然当前设计中家属端只读,但未来可能支持家属端添加备注)。冲突解决策略设计如下:
-
Last Write Wins (LWW):对于简单值(液位、流速、状态),使用最后写入胜出策略。每个写入附带时间戳,后写入的值覆盖先写入的值。这适用于单调更新的数据。
-
Merge by Timestamp:对于集合类数据(预警记录),使用时间戳合并策略。冲突时将两端的记录按时间戳排序去重,保留所有唯一记录。由于每条预警的时间戳是唯一的(毫秒级精度),去重操作简单可靠。
-
Application-Level Resolution:对于复杂冲突(如家属端添加了备注而患者端删除了预警),需要应用层的冲突解决逻辑。这类冲突在实践中极少发生,可以在后续版本中按需实现。
KV Store与AppStorage的桥接:
RemoteDataSync类的processRemoteEntry方法展示了KV Store数据如何桥接到AppStorage——当KV Store中的数据变更时,将变更同步到AppStorage,触发UI更新。这种桥接模式使得上层业务代码(FamilyHomePage等)无需感知数据来源是本地还是远程,降低代码耦合度。
4.3 实时性要求与延迟容忍
不同类型的数据对实时性有不同的要求。我们在设计时区分了三个实时性等级:
等级一:紧急预警(5秒内):
- 液位低于20%的红色预警
- 设备异常预警
- 管路堵塞预警
这类预警直接关系到患者安全,延迟容忍度最低。5秒的要求考虑了以下链路时间:
- 传感器采集:1秒
- 患者端处理和写入:1秒
- 网络传输:1-2秒
- 家属端接收和渲染:1秒
5秒是一个现实的、可验证的目标。超过5秒的延迟意味着家属可能在患者端触发预警后6-7秒才看到通知,在紧急情况下这段时间可能影响处理时效。
实现路径:分布式KV Store的dataChange回调 + 系统通知API。当KV Store数据变更时,家属端立即收到回调,同时通过系统通知API发送本地通知,即使应用在后台也能引起注意。
等级二:状态更新(30秒内):
- 液位百分比变化
- 流速变化
- 会话状态变更
这类数据的变化是渐进的,30秒的延迟对决策没有实质性影响。例如,液位从65%变到64%,即使家属端30秒后才看到这个变化,也不会影响任何判断和行动。
30秒的更新间隔也是流量和电量的平衡点——过于频繁的同步会消耗设备和网络资源,过于稀疏的同步又会让家属觉得数据"不新鲜"。
实现路径:定时轮询(患者端每30秒写入一次,家属端aboutToAppear时读取)或KV Store自动同步。MVP版本使用前者,未来版本使用后者。
等级三:历史记录(5分钟内):
- 预警历史列表
- 输液会话记录
- 统计数据
这类数据用于事后查看和分析,对实时性要求最低。5分钟的延迟容忍度意味着家属查看预警记录时,可能看不到最近5分钟内的新预警,但这在查看"历史记录"的场景中是可以接受的。
实现路径:KV Store批量同步或HTTP API定时拉取。未来版本中,这类数据可能通过云端API提供,而非设备间直连同步。
实时性分级总结:
| 等级 | 数据类型 | 延迟容忍 | 同步方式 | 通知方式 |
|---|---|---|---|---|
| P0 | 红色预警 | <=5秒 | KV Store实时 | 系统通知+震动 |
| P1 | 状态数据 | <=30秒 | KV Store/轮询 | 应用内更新 |
| P2 | 历史记录 | <=5分钟 | KV Store/API | 无需通知 |
延迟容忍度的用户体验影响:
每个实时性等级的延迟容忍度都经过了用户心理学和医疗实践的考量:
- P0的5秒容忍度基于"注意力窗口"理论——人对突发事件的注意力窗口约为3-5秒,超过这个时间的延迟会被感知为"慢"
- P1的30秒容忍度基于"更新频率预期"——人对状态数据的更新频率预期约为30秒,与心跳监测仪的刷新频率相当
- P2的5分钟容忍度基于"历史回顾"场景——查看历史记录的用户通常不需要实时数据,5分钟的延迟不影响信息理解
这些容忍度不是绝对的上限,而是设计目标。实际实现中,应该努力将延迟控制在容忍度的一半以下,为异常情况(如网络波动)留出缓冲。
5. 家属端信息脱敏
医疗信息安全是IVGuard系统设计中不可逾越的红线。家属端作为面向非医疗专业人员的界面,需要在信息透明和信息保护之间取得精确平衡。
5.1 仅显示必要信息
家属端遵循"最小必要信息"原则——只展示家属关心且有权知道的信息,不展示任何超出范围的数据。
显示的信息:
| 信息项 | 展示方式 | 必要性论证 |
|---|---|---|
| 患者姓名 | 文字 | 确认身份,避免看错患者 |
| 床号 | 文字 | 定位患者位置 |
| 当前液位 | 环形进度+百分比 | 核心监控数据 |
| 药品名称 | 文字 | 基本知情权 |
| 流速 | 文字 | 辅助判断状态 |
| 预警信息 | 时间线列表 | 安全通知 |
| 护士站位置 | 导航页面 | 联系护士 |
不显示的信息:
| 信息项 | 隐藏原因 | 存在位置 |
|---|---|---|
| 其他患者数据 | 隐私保护 | 服务端/仅护士端 |
| 护士内部管理数据 | 无关+敏感 | 仅护士端 |
| 医保详情 | 隐私+敏感 | 仅患者端 |
| 药品剂量详情 | 无需知晓 | 仅患者端/护士端 |
| 输液管参数 | 技术细节 | 仅护士端 |
| 护士排班信息 | 无关+隐私 | 仅护士端 |
最小必要信息原则的法律依据:
在中国的《个人信息保护法》和《信息安全技术 个人信息安全规范》中,"最小必要"是个人信息处理的基本原则之一。在医疗场景中,这一原则的适用更为严格——医疗健康信息属于敏感个人信息,其处理需要取得个人的单独同意。
IVGuard家属端的信息展示设计,从源头就遵循了"最小必要"原则,而不是先展示所有信息再加权限控制。这种"默认最小"的设计策略,在合规性和安全性上都优于"默认全量+权限过滤"的策略。
5.2 不暴露其他患者数据
在病房环境中,一台设备或一个护士站可能管理多个患者的输液数据。家属端的数据访问必须严格限定在"自己的亲人"范围内,绝不能出现A患者的家属看到B患者数据的情况。
数据隔离机制:
``typescript
loadData() {
const currentSession = DataStore.getCurrentSession()
if (!currentSession) {
return
}
this.patientName = currentSession.patientName
this.bedNumber = currentSession.bedNumber
this.currentLevel = DataStore.getCurrentLevel()
this.medicineName = currentSession.medicineName
this.flowRate = DataStore.getFlowRate()
this.recentAlerts = DataStore.getAlerts()
.filter((alert: AlertInfo) => alert.sessionId === currentSession.id)
}
``
关键点是filter操作——即使DataStore中存储了多个会话的预警数据(未来版本可能支持多患者监控),家属端也只加载当前会话相关的数据。sessionId作为过滤条件,确保数据隔离在应用层就得到保障。
多患者场景的隔离设计:
在护士端,一个护士可能同时监控多个患者的输液。但家属端的数据视角应该是"单患者"的——每个家属只关心自己的亲人。在数据架构上,这意味着:
- 家属端登录时需要绑定特定的患者ID
- 所有数据查询都以患者ID为过滤条件
- DataStore中虽然有全局数据,但家属端只访问自己绑定的患者数据
这种"登录即绑定"的数据隔离模型,在多用户系统中是标准做法。MVP版本中由于是单设备单用户模型,暂时不需要登录机制,但数据架构已经预留了sessionId字段,为未来的多用户扩展做好了准备。
5.3 不显示护士内部管理数据
护士端有许多内部管理功能——排班、交接班记录、患者评估量表、护理计划等。这些数据对家属没有价值,且可能包含敏感信息(如护士对患者的护理评估可能包含主观判断,不适合向家属展示)。
数据隔离通过"页面级"而非"组件级"实现——家属端和护士端是完全不同的页面路由,家属端根本没有加载护士端页面的入口。这种"物理隔离"比"逻辑隔离"(如权限控制)更安全——即使权限系统出现bug,家属端也无法访问护士端的数据,因为那些代码根本没有被加载。
物理隔离的优势:
| 隔离方式 | 实现机制 | 安全性 | 可维护性 |
|---|---|---|---|
| 物理隔离 | 独立路由/独立模块 | 高(代码级隔离) | 高(无耦合) |
| 逻辑隔离 | 权限控制/条件渲染 | 中(依赖权限正确性) | 低(权限逻辑分散) |
| 数据隔离 | 查询过滤/视图控制 | 中低(依赖过滤正确性) | 中(需维护过滤逻辑) |
IVGuard选择物理隔离作为护士数据与家属数据之间的隔离方式,是因为物理隔离的"最小权限"属性更强——不是"有权限但不使用",而是"根本没有权限"。
5.4 医保详情仅患者端可见
医保信息(医保类型、报销比例、个人账户余额等)属于高度敏感的个人隐私信息。即使家属是患者的亲人,也不意味着他们有权查看患者的医保信息——现实中,很多患者不希望家属知道自己的医保余额或报销情况。
IVGuard将费用管理功能完全限定在患者端:
- 家属端没有"费用管理"入口
- 家属端的导航路由中不包含费用相关页面
- DataStore中与费用相关的Key不会在家属端被读取
这种设计遵循了"数据最小暴露"原则——每个角色只能访问其职能范围内的最小数据集。即使同一台设备、同一个应用,不同角色看到的数据范围也是不同的。
费用信息的特殊敏感性:
在医疗场景中,费用信息是一个敏感的社会问题。家属看到高额费用可能产生焦虑或争议,患者可能不希望家属知道自己的医疗支出。将费用信息限定在患者端,不仅保护了患者隐私,也避免了可能的家庭纠纷——这是医疗应用设计中一个容易被忽视但实际很重要的社会因素。
5.5 脱敏实现的技术保障
typescript function sanitizeForFamily(data: SessionInfo): FamilyViewData { return { patientName: data.patientName, bedNumber: data.bedNumber, currentLevel: data.currentLevel, medicineName: data.medicineName, flowRate: data.flowRate, alertStatus: data.alertStatus // 不包含: insuranceInfo, dosageDetails, nurseNotes, etc. } }
脱敏函数sanitizeForFamily是家属端数据访问的必经之路。任何从DataStore读取的数据,在传递给家属端UI组件之前,都经过这个函数的过滤。这种"中间层脱敏"模式确保了即使开发者误将敏感数据绑定到了UI组件,脱敏函数也会将其过滤掉。
脱敏函数的设计原则:
-
白名单模式:只列出允许展示的字段,未列出的字段一律不展示。这比"黑名单模式"(列出不允许展示的字段)更安全——新增字段时不会意外暴露敏感数据。
-
类型转换:脱敏函数不仅过滤字段,还可能转换数据类型。例如,将Date类型转换为格式化的时间字符串,避免暴露原始时间戳。
-
集中管理:脱敏逻辑集中在一个函数中,而不是分散在各个组件中。这保证了脱敏策略的一致性和可审计性。
FamilyViewData类型定义:
typescript interface FamilyViewData { patientName: string bedNumber: string currentLevel: number medicineName: string flowRate: number alertStatus: string }
FamilyViewData是一个专门为家属端定义的数据类型,它是SessionInfo的子集。这种类型级别的隔离比运行时过滤更安全——TypeScript编译器会在开发阶段就阻止家属端代码访问不在FamilyViewData中的字段。
5.6 脱敏与用户体验的平衡
信息脱敏的目的是保护隐私,但如果过度脱敏导致家属端信息不足,就违背了"关键信息一览"的设计初衷。需要在脱敏和可用之间找到平衡点。
平衡的判断标准:家属端展示的每一条信息都应该满足以下条件之一:
- 家属需要此信息来做出决策(如液位低于20%需要联系护士)
- 家属有合法的知情权(如知道自己亲人正在输什么药)
- 此信息对安全保障有贡献(如预警通知)
不满足以上任何条件的信息,即使无害也不应该展示——这不是"隐藏",而是"简洁"。家属端的信息密度应该与家属的信息处理能力匹配,而不是与技术系统的数据丰富度匹配。
6. 情感化设计
医疗场景中的UI设计,不仅是信息传达,更是情感管理。家属端的用户——患者家属——通常处于焦虑、担忧的心理状态中。每一个设计决策都需要考虑它对用户情绪的影响。
6.1 状态颜色给家属安全感
颜色是最直观的情感触发器。IVGuard家属端的三色状态系统不仅是信息分类工具,更是情绪调节工具。
绿色=安心:
当家属看到绿色指示灯时,他们不需要进行任何认知处理——绿色直接触发"安全"的情感反应。这是人类在长期进化中形成的颜色-情感关联:绿色代表自然界中的丰饶和生机,代表交通信号中的"通行",代表医疗场景中的"正常"。
设计上,绿色不仅在指示灯上使用,还在空状态文案中使用:"暂无预警,一切正常"使用绿色文字,强化"没有预警是好事"的正向情感。
绿色在医院环境中的特殊意义:在医院的视觉环境中,绿色是"安全"的代名词——手术室的无影灯是绿色的,心电监护仪的正常波形是绿色的,呼吸机的正常指示灯也是绿色的。IVGuard的绿色状态灯与医院环境的视觉语言保持一致,降低了家属的认知转换成本。
黄色=关注(非恐惧):
黄色在传统UI中常被用于"警告",但在家属端,我们刻意避免使用"警告"这个词。黄色传达的是"请注意"而非"请害怕"——液位较低并不意味着危险,只是意味着需要开始关注。这种语义上的微妙差异,对家属的情绪影响是巨大的。
黄色与其他颜色的搭配:预警摘要区域使用浅橙色背景(#FFF3E0)而非黄色背景,是因为纯黄色背景在移动端屏幕上可能过于刺眼,尤其在夜间使用时。浅橙色背景既传达了"注意"的信息,又不会造成视觉不适。
红色=行动(非恐慌):
红色在自然语言中常常与"危险"、“紧急"等强烈的负面词汇关联。但在家属端,我们选择将红色的语义限定为"需要行动"而非"正在危险中”。文案上,我们使用"需要关注"而非"危险",使用"建议联系护士"而非"立即报警"。
这种"去恐吓化"的颜色使用策略,可以减少家属在看到红色状态时的恐慌反应,使其保持理性判断和有效行动。一个恐慌的家属可能做出不理性的决定(如匆忙赶往医院途中发生交通事故),而一个"需要关注"的家属更可能冷静地联系护士、了解情况、做出合理的判断。
颜色与行动的映射关系:
| 颜色 | 信息语义 | 情感目标 | 建议行动 |
|---|---|---|---|
| 绿色 | 一切正常 | 安心 | 无需行动 |
| 黄色 | 需要关注 | 留意 | 可以联系护士 |
| 红色 | 需要关注 | 行动 | 建议联系护士 |
注意红色和黄色的建议行动差异——黄色是"可以",红色是"建议"。这种语气上的差异比"必须"vs"可选"更温和,避免了给家属制造紧迫感。
6.2 简洁文案减少焦虑
文案是UI设计中最容易被忽视但影响最大的元素之一。在家属端,每一句文案都经过精心斟酌,避免增加不必要的焦虑。
负面词汇替换表:
| 原始表达 | 替换表达 | 理由 |
|---|---|---|
| 危险 | 需要关注 | "危险"引发恐慌,"需要关注"引发行动 |
| 警告 | 提示 | "警告"有恐吓感,"提示"更中性 |
| 异常 | 变化 | "异常"暗示有问题,"变化"更中性 |
| 故障 | 调整中 | "故障"让人担心设备坏了,"调整中"更安心 |
| 紧急 | 请尽快 | "紧急"制造压迫感,"请尽快"更有序 |
| 失败 | 未成功 | "失败"负面,"未成功"中性 |
| 停止 | 暂停 | "停止"暗示终结,"暂停"暗示可恢复 |
文案设计的原则体系:
-
中性优先:选择中性而非极端的词汇。"变化"比"异常"更中性,"调整中"比"故障"更中性。中性文案传达事实而不传达情绪。
-
行动导向:文案应该引导用户采取行动,而不是描述问题的严重性。“建议联系护士"比"情况严重"更有价值,因为它告诉用户"该做什么"而非"事情有多糟”。
-
避免绝对:避免使用"一定"、“绝不”、"永远"等绝对性词汇。医疗场景中很少有绝对的事情,绝对性词汇会增加用户的心理压力。
-
尊重用户:不使用居高临下的语气(如"请注意"、“务必”),而是使用建议性的语气(如"建议"、“可以考虑”)。这种尊重性的语气让家属感到自己有选择权,而不是被命令。
文案长度控制:
家属端的文案遵循"越短越好"的原则。在焦虑状态下,人的阅读耐心下降,长文案容易被跳过或误解。例如:
- 冗长文案:“当前输液液位已低于安全阈值,建议您尽快联系负责护士进行处理”
- 精简文案:“液位较低,建议联系护士”
短文案不仅更容易阅读,也更容易在手机屏幕上展示完整,不会出现截断需要展开的情况。
文案的A/B测试建议:
在未来版本中,建议对关键文案进行A/B测试,验证不同表达方式对用户情绪和行为的影响。例如:
- A版本:“液位较低” vs B版本:“液位需要关注”
- A版本:“联系护士” vs B版本:“与护士沟通”
- A版本:“需要关注” vs B版本:“请留意”
通过A/B测试选择对用户最友好、最有效的文案表达,是数据驱动的情感化设计方法论。
6.3 快捷操作减少步骤
在家属端,操作的步骤数与用户的心理负担成正比——每多一步操作,就多一分犹豫和焦虑的空间。"联系护士"按钮的设计就是这一原则的典型体现。
操作步骤对比:
| 操作 | 传统方式 | IVGuard方式 | 步骤减少 |
|---|---|---|---|
| 联系护士 | 找到护士站->询问护士姓名->拨打总机->转接 | 点击"联系护士" | 4步到1步 |
| 查看液位 | 走到输液架旁->观察液面->估算比例 | 打开App->自动显示 | 3步到1步 |
| 查看预警 | 等护士告知->或守在旁边观察 | 打开App->预警自动推送 | 被动到主动 |
每减少一个操作步骤,就减少一个可能的犹豫点。在紧急情况下,这种"零犹豫"设计可能是至关重要的——当家属看到红色预警时,他们不需要思考"怎么联系护士",只需要点击那个醒目的橙色按钮。
一步操作的设计范式:
家属端的操作设计遵循"一步范式"——每个操作都应该在一步内完成:
- 查看状态:打开App即看到,无需任何操作
- 联系护士:点击一个按钮,无需选择或填写
- 查看预警:自动推送,无需主动搜索
- 切换角色:点击一个链接,无需退出和重新登录
这种"一步范式"不仅减少了操作步骤,也降低了学习成本——用户不需要记忆复杂的操作流程,每个操作都是直觉性的。
减少步骤的技术实现:
``typescript
// 传统方式:多步骤联系护士
// Step 1: 查找护士信息
findNurseInfo(patientId: string) { … }
// Step 2: 选择联系方式
selectContactMethod(nurseId: string) { … }
// Step 3: 拨打电话
makePhoneCall(phoneNumber: string) { … }
// IVGuard方式:一步联系护士
contactNurse() {
this.navController.pushPath({ name: ‘HospitalNavPage’ })
}
``
技术实现上,"一步操作"意味着将多个子步骤封装在一个函数/方法中,对外只暴露一个简单的调用接口。这种封装不仅简化了用户操作,也降低了代码的耦合度——内部实现可以随时变化(从页面跳转到直接拨号),而不影响调用方。
6.4 预警分级不造成恐慌
预警的分级展示是情感化设计中最精细的部分。IVGuard将预警分为"注意"(黄色)和"紧急"(红色)两级,而非更常见的"提示/警告/严重/致命"四级体系。这种简化分级的决策基于以下考量:
两级vs四级:
四级体系虽然信息更精确,但要求用户理解四个级别的差异——"提示"和"警告"有什么区别?"严重"和"致命"的行动方案有什么不同?在家属端的场景中,这种细分反而增加了认知负担。
两级体系更简单直观:
- 注意:看看就好,可以联系护士了解情况
- 紧急:立刻行动,联系护士
这种"二元决策"模型——要么注意,要么行动——消除了灰色地带,让家属不需要判断"这个预警到底严不严重"。
文案中的级别弱化:
即使在家属端显示预警级别,也避免使用"严重"、“危险"等词汇。预警列表中的分类标签是"液位偏低"而非"液位危险低”,是"流速变化"而非"流速严重异常"。这种措辞的差异看似微小,但对家属情绪的累积影响是显著的——整个家属端从头到尾不使用任何恐吓性词汇,营造出一个"信息透明但不恐吓"的氛围。
预警去重与静默:
当同一条预警在短时间内反复触发时(如液位在19%-21%之间波动,反复跨越20%的红色阈值),系统应该进行去重处理,避免短时间内向家属推送大量内容相似的预警。去重规则设计如下:
- 同一类型的预警,5分钟内只推送一次
- 如果预警级别升级(从黄色变为红色),立即推送升级通知
- 如果预警级别降级(从红色变为黄色),静默更新,不推送通知
这种设计避免了"预警风暴"——短时间内的大量预警会让家属产生恐慌,即使每条预警本身并不严重。去重和静默机制让预警保持"有意义"而非"烦人"。
去重的实现设计:
``typescript
class AlertDeduplicator {
private lastAlertTime: Map<string, number> = new Map()
private readonly DEDUP_INTERVAL = 5 * 60 * 1000 // 5分钟
shouldEmit(alertType: string, severity: string, lastSeverity: string): boolean {
const key = alertType
const now = Date.now()
const lastTime = this.lastAlertTime.get(key) || 0
// 级别升级:立即推送
if (severity === 'high' && lastSeverity !== 'high') {
this.lastAlertTime.set(key, now)
return true
}
// 级别降级:静默更新
if (severity !== 'high' && lastSeverity === 'high') {
return false
}
// 同级别:5分钟内去重
if (now - lastTime < this.DEDUP_INTERVAL) {
return false
}
this.lastAlertTime.set(key, now)
return true
}
}
``
这个去重器的设计简洁而有效——它为每种预警类型维护最后推送时间,在去重间隔内不重复推送。同时,级别升级打破去重间隔(紧急情况不应该被去重延迟),级别降级始终静默(好事不需要反复通知)。
7. 家属端交互流程
完整的交互流程设计确保每个使用场景都有清晰的页面流转和操作路径。
7.1 场景一:正常监控流程
打开App -> 选择"我是家属" -> FamilyHomePage | 查看亲人状态 -> 绿色指示灯 -> 安心等待 | 关闭App / 切换到其他应用 -> 绿色指示灯持续显示 | 再次打开App -> 状态自动刷新 -> 继续监控
这是最常见的使用场景——家属打开App查看状态,看到绿色指示灯后安心地去做其他事情。整个交互流程在3秒内完成:打开App(1秒)->看到绿色指示灯(1秒)->关闭App(1秒)。这种"秒开秒看秒走"的使用模式,要求FamilyHomePage的信息展示必须即时加载,不能有加载动画或骨架屏之类的延迟。
正常监控的心理模型:
家属在这个场景中的心理状态是"确认安全后离开"——类似家长查看婴儿监视器的行为模式。他们不需要持续关注,只需要间歇性地确认"一切正常"。这种使用模式对UI设计有以下启示:
- 首屏即全部:所有关键信息必须在首屏展示完毕,不需要滚动
- 状态颜色优先:用户最先看到的是颜色,不是数字或文字
- 快速退出:不需要"退出确认"之类的阻碍,用户看完就走
7.2 场景二:黄色预警响应
查看亲人状态 -> 黄色指示灯 -> 心理准备 | 查看液位详情 -> 还剩约35% -> 预计还有20分钟 | 查看最近预警 -> "液位偏低" -> 了解原因 | 考虑是否联系护士 -> 暂时不急 -> 继续关注 | (或者)决定联系护士 -> 点击"联系护士" -> HospitalNavPage
黄色预警场景下,家属的心理是"需要留意但不必惊慌"。交互流程给予家属足够的信息做判断——液位还剩多少、预计还有多久、为什么会有这个预警。有了这些信息,家属可以自主决定是"继续关注"还是"联系护士",而不是被动地不知道该怎么办。
黄色预警的决策树:
黄色预警 | +--> 了解详情(液位/流速/预警消息) | | | +--> 判断是否紧急 | | | +--> 不紧急 -> 继续关注,等待自动恢复绿色 | | | +--> 担心 -> 点击"联系护士" | +--> 不关心详情 -> 继续关注,等待自动恢复绿色
这个决策树展示了家属在黄色预警下的两种可能路径——“了解详情后决策"和"直接忽略”。UI设计需要同时支持这两种路径,不应该强制用户了解详情才能继续。
7.3 场景三:红色预警紧急响应
收到红色预警通知 -> 立即打开App | 看到红色指示灯 -> "需要关注" | 立即点击"联系护士" -> HospitalNavPage | (未来版本)直接拨号 -> 与护士通话
红色预警是最高优先级的场景,整个交互流程的设计目标是"从看到预警到采取行动的路径最短"。关键设计点:
- 系统通知必须足够醒目(震动+提示音),确保家属注意到
- 点击通知直接跳转到FamilyHomePage,不需要额外的导航
- "联系护士"按钮在页面显眼位置,不需要搜索或滚动
- 联系方式一步到位,不需要选择或填写
红色预警的响应时间分析:
0s: 患者端触发预警 | 1s: 数据写入DataStore/KV Store | 2-3s: 家属端收到数据变更通知 | 3-4s: 系统通知显示在通知栏 | 4-5s: 家属注意到通知 | 5-6s: 家属点击通知,App打开 | 6-7s: FamilyHomePage显示,红色状态灯可见 | 7-8s: 家属点击"联系护士" | 8-10s: 跳转到HospitalNavPage / 开始拨号
从预警触发到家属采取行动,理想情况下在10秒内完成。这个时间取决于家属的响应速度(4-5秒注意到通知是平均水平),系统侧的延迟应控制在5秒以内。
红色预警通知的设计细节:
typescript // 系统通知配置(未来版本) const notificationRequest: notificationManager.NotificationRequest = { id: 1, content: { contentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT, basicContent: { title: 'IVGuard 输液提醒', text: '液位较低,建议联系护士', } }, wantAgent: wantAgent.getWantAgent({ uri: 'ivguard://family_home' }), slotType: notificationManager.SlotType.SOCIAL_COMMUNICATION }
通知文案使用"建议联系护士"而非"液位紧急",避免通知本身造成恐慌。wantAgent配置使得点击通知直接跳转到FamilyHomePage,减少中间步骤。slotType选择SOCIAL_COMMUNICATION而非EMERGENCY,是因为我们的应用不是紧急服务,不应该使用系统级的紧急通知通道。
7.4 场景四:查看预警记录
FamilyHomePage -> 点击"查看全部预警记录" | FamilyAlertPage -> 时间线列表 | 浏览预警历史 -> 了解输液过程中的所有异常 | (返回)-> FamilyHomePage
查看预警记录是一个"信息补充"操作——家属在首页看到最近3条预警后,如果想了解更早的历史,才需要进入预警记录页面。这个流程不是高频操作,但它的存在让家属有了"可追溯"的安全感——所有异常都有记录可查,不会遗漏。
预警记录页面的返回体验:
从FamilyAlertPage返回FamilyHomePage使用Navigation的默认返回行为(左上角返回按钮或手势返回)。返回后FamilyHomePage会重新执行aboutToAppear,自动刷新数据。这种"返回即刷新"的行为确保了数据的时效性。
7.5 场景五:角色切换
FamilyHomePage -> 点击"切换到患者端" | PatientHomePage -> 以患者身份使用 | (切换回来)PatientHomePage -> 点击"切换到家属端" | FamilyHomePage -> 回到家属视角
角色切换是MVP版本的特殊流程——因为同一台设备上只有一个应用实例,家属和患者需要共享设备。角色切换使用
eplacePath而非pushPath,避免导航栈中累积多个页面实例。切换后数据自动刷新,因为aboutToAppear生命周期会在页面加载时重新读取DataStore。
角色切换的使用场景:
- 患者本人操作设备后,家属想要查看监控状态
- 家属在查看监控时,患者需要操作某些功能(如暂停输液)
- 演示/测试时在不同角色间切换
在实际使用中,角色切换应该是一个低频操作。理想的场景是患者端运行在床头设备上,家属端运行在家属的手机上,两者独立运行、数据自动同步。但这需要分布式数据同步的支持,属于未来版本的范围。
角色切换的UI设计考量:
角色切换入口在页面底部,使用灰色小字,视觉权重很低。这种"低调"的设计是基于以下考量:
- 角色切换是低频操作,不应该占据显眼位置
- 过于显眼的切换入口可能让用户困惑——“我是谁?我现在在哪个角色?”
- 低视觉权重暗示"这不是主要功能,只是辅助功能"
7.6 交互流程状态图
+-----------------+ | App启动 | +--------+--------+ | +--------v--------+ | 选择角色 | | 患者端 / 家属端 | +---+---------+---+ | | +------------v---+ +--v------------+ | PatientHomePage| | FamilyHomePage |<------+ +----------------+ +-------+--------+ | | | +------------+------------+ | | | | | +-----v-----+ +---v-----+ +---v----+--+ |联系护士 | |预警记录 | |切换角色 | |NavPage | |AlertPage| | | +-----------+ +---------+ +-----------+
状态图的解读:
- App启动后,用户首先选择角色(患者端或家属端)
- 选择家属端后进入FamilyHomePage,这是家属端的主状态
- 从FamilyHomePage可以进入三个子状态:联系护士、预警记录、切换角色
- 切换角色后进入PatientHomePage,无法再返回FamilyHomePage(因为使用了replacePath)
- 从预警记录和护士导航页面可以返回FamilyHomePage
导航栈分析:
| 操作 | 导航方式 | 导航栈变化 |
|---|---|---|
| 选择家属端 | replacePath | [FamilyHomePage] |
| 查看预警记录 | pushPath | [FamilyHomePage, FamilyAlertPage] |
| 返回首页 | popPath | [FamilyHomePage] |
| 联系护士 | pushPath | [FamilyHomePage, HospitalNavPage] |
| 返回首页 | popPath | [FamilyHomePage] |
| 切换角色 | replacePath | [PatientHomePage] |
使用
eplacePath进行角色切换确保了导航栈中始终只有一个"首页"级别的页面,避免了"FamilyHomePage -> PatientHomePage -> FamilyHomePage -> …"的无限栈增长。
8. 数据加载与刷新
数据加载和刷新机制直接影响家属端的信息时效性。本章从生命周期管理、加载流程和刷新策略三个维度进行解析。
8.1 aboutToAppear生命周期
typescript aboutToAppear() { DataStore.init() this.loadData() }
aboutToAppear是ArkUI组件的生命周期回调,在组件即将出现在屏幕上之前调用。它是最适合执行数据加载的时机——早于onPageShow(页面已经显示但数据还没加载完会出现空白),晚于aboutToDisappear(那是清理资源用的,不是加载数据的)。
**DataStore.init()**的调用确保在读取数据之前,所有必要的PersistentStorage属性已经被声明。这避免了"读取一个不存在的Key返回undefined"的问题。
**this.loadData()**在init之后调用,确保数据访问的依赖关系正确。
ArkUI生命周期与数据加载的关系:
| 生命周期 | 时机 | 适合做什么 | 不适合做什么 |
|---|---|---|---|
| aboutToAppear | 组件即将出现 | 初始化数据、注册监听 | 更新UI(组件尚未渲染) |
| onAppear | 组件已出现 | 启动动画、上报埋点 | 数据加载(可能闪烁) |
| onPageShow | 页面显示 | 刷新数据、恢复状态 | 初始化(可能重复执行) |
| aboutToDisappear | 组件即将消失 | 清理资源、取消监听 | 数据加载(组件即将销毁) |
选择aboutToAppear而非onPageShow进行数据加载,是因为aboutToAppear在组件首次创建时只执行一次,而onPageShow在页面每次显示时都会执行(包括从其他页面返回时)。对于家属端,我们希望每次进入页面都刷新数据,所以实际上应该在onPageShow中执行数据刷新。但在MVP版本中,aboutToAppear的行为已经满足了基本需求——因为角色切换使用
eplacePath,每次切换都会重新创建组件。
8.2 loadData加载流程
``typescript
loadData() {
const sessions = DataStore.getSessions()
const currentSession = sessions.find((s: SessionInfo) => s.status === ‘running’)
if (currentSession) {
this.patientName = currentSession.patientName
this.bedNumber = currentSession.bedNumber
this.medicineName = currentSession.medicineName
this.sessionId = currentSession.id
}
this.currentLevel = DataStore.getCurrentLevel()
this.flowRate = DataStore.getFlowRate()
const allAlerts = DataStore.getAlerts()
this.recentAlerts = allAlerts
.filter((a: AlertInfo) => a.sessionId === this.sessionId)
.sort((a: AlertInfo, b: AlertInfo) => b.timestamp - a.timestamp)
this.updateAlertStatus()
}
``
加载流程分步骤解析:
步骤一:获取会话信息:
typescript const sessions = DataStore.getSessions() const currentSession = sessions.find((s: SessionInfo) => s.status === 'running')
从DataStore读取所有输液会话,然后找到状态为"running"的当前会话。find方法返回第一个匹配的元素,在MVP版本中同一时间只有一个运行中的会话。如果找不到运行中的会话(如输液已经结束或尚未开始),currentSession为undefined,后续的条件判断会处理这种情况。
步骤二:填充患者信息:
typescript if (currentSession) { this.patientName = currentSession.patientName this.bedNumber = currentSession.bedNumber this.medicineName = currentSession.medicineName this.sessionId = currentSession.id }
只有当存在运行中的会话时,才填充患者信息。如果没有运行中的会话,患者相关字段保持初始值(空字符串),UI上会显示空白。这是一种安全的降级策略——宁可显示空白也不显示错误信息。
步骤三:读取实时数据:
typescript this.currentLevel = DataStore.getCurrentLevel() this.flowRate = DataStore.getFlowRate()
液位和流速直接从DataStore读取。这两个值是由患者端的模拟数据源不断更新的,家属端每次进入页面时读取最新值。getCurrentLevel()和getFlowRate()都提供了默认值(100和2.0),确保在DataStore中尚未写入数据时也能正常显示。
步骤四:加载预警数据:
typescript const allAlerts = DataStore.getAlerts() this.recentAlerts = allAlerts .filter((a: AlertInfo) => a.sessionId === this.sessionId) .sort((a: AlertInfo, b: AlertInfo) => b.timestamp - a.timestamp)
预警数据先通过sessionId过滤(确保只显示当前会话的预警),然后按时间戳降序排序(最新的在前)。filter + sort的组合是一个常见的数据处理模式——先筛选再排序,保证数据的范围正确和顺序合理。
步骤五:更新状态指示灯:
typescript this.updateAlertStatus()
最后调用updateAlertStatus(),根据当前液位更新状态指示灯的颜色。这一步必须在currentLevel被赋值之后执行,否则updateAlertStatus()读取的可能是初始值0,导致状态指示灯错误地显示红色。
loadData的执行时序:
aboutToAppear() +-- DataStore.init() // 确保持久化属性已声明 +-- loadData() +-- getSessions() // 读取会话列表 +-- find(running) // 定位当前会话 +-- getCurrentLevel() // 读取液位 +-- getFlowRate() // 读取流速 +-- getAlerts() // 读取预警 +-- filter + sort // 过滤排序 +-- updateAlertStatus() // 更新状态灯
这个时序设计确保了数据依赖关系的正确性——每一步都依赖前一步的结果,不存在"先读后写"或"读到了未初始化的数据"的风险。
8.3 刷新机制
当前MVP版本的刷新机制是"每次进入页面刷新"——当用户从其他页面返回FamilyHomePage时,aboutToAppear会再次执行,重新加载数据。
这种"重新进入即刷新"的策略简单可靠,但在某些场景下可能不够及时:
-
用户一直停留在FamilyHomePage:如果家属一直盯着首页看,不离开页面,那么数据不会自动刷新。液位在持续下降,但UI不会更新,直到用户切换到其他页面再切回来。
-
后台数据变化:当应用在后台时,患者端的数据可能在持续变化,但家属端回到前台后需要等待aboutToAppear执行完毕才能看到最新数据。
MVP版本的折中方案:当前版本不做定时刷新,因为MVP的数据源是模拟数据(MockData),液位变化是通过手动操作触发的,不需要自动刷新。但在正式产品中,必须实现更完善的刷新机制。
未来刷新方案:
``typescript
// 方案一:定时器轮询
private refreshTimer: number = -1
aboutToAppear() {
DataStore.init()
this.loadData()
this.refreshTimer = setInterval(() => {
this.loadData()
}, 30000) // 每30秒刷新一次
}
aboutToDisappear() {
clearInterval(this.refreshTimer)
}
``
定时器轮询是最简单的自动刷新方案,每30秒重新加载一次数据。优点是实现简单,缺点是:
- 刷新间隔固定,无论数据是否变化都会刷新
- 应用在后台时定时器可能被系统暂停
- 频繁的JSON.parse可能影响性能
typescript // 方案二:AppStorage响应式绑定 @StorageProp('ivguard_current_level') currentLevel: number = 100 @StorageProp('ivguard_flow_rate') flowRate: number = 2.0
方案二使用@StorageProp装饰器直接绑定AppStorage中的值,当AppStorage中的值变化时,UI自动更新,不需要手动刷新。这是ArkUI推荐的响应式数据绑定方式,比定时器轮询更高效、更及时。
@StorageProp的工作原理:当AppStorage中对应的Key的值发生变化时,ArkUI框架会自动重新渲染绑定了该Key的UI组件。这种机制在组件级别实现了"数据驱动UI",无需手动调用loadData()。
``typescript
// 方案三:WebSocket实时推送(远期)
private webSocket: WebSocket | null = null
async connectWebSocket() {
this.webSocket = webSocket.createWebSocket(‘wss://ivguard.com/ws’)
this.webSocket.on(‘message’, (event: MessageEvent) => {
const data = JSON.parse(event.data as string)
if (data.type === ‘level_update’) {
this.currentLevel = data.level
} else if (data.type === ‘alert’) {
this.recentAlerts.unshift(data.alert)
}
this.updateAlertStatus()
})
}
``
WebSocket方案提供真正的实时数据推送,是最终目标的实现方式。当患者端的数据发生变化时,服务端立即通过WebSocket推送到家属端,延迟在毫秒级。但WebSocket的实现需要后端服务的支持,属于远期规划。
三种刷新方案的对比:
| 方案 | 实时性 | 性能开销 | 实现复杂度 | 适用阶段 |
|---|---|---|---|---|
| 页面进入刷新 | 低(秒级) | 低 | 低 | MVP |
| 定时器轮询 | 中(30秒级) | 中 | 低 | MVP+ |
| @StorageProp绑定 | 高(即时) | 低 | 中 | V1.0 |
| WebSocket推送 | 极高(毫秒级) | 高 | 高 | V2.0 |
8.4 数据加载的错误处理
在实际使用中,数据加载可能因为各种原因失败——DataStore未初始化、数据格式损坏、存储空间不足等。健壮的错误处理是必须的:
``typescript
loadData() {
try {
const sessions = DataStore.getSessions()
const currentSession = sessions.find((s: SessionInfo) => s.status === ‘running’)
if (currentSession) {
this.patientName = currentSession.patientName
this.bedNumber = currentSession.bedNumber
this.medicineName = currentSession.medicineName
this.sessionId = currentSession.id
}
this.currentLevel = DataStore.getCurrentLevel()
this.flowRate = DataStore.getFlowRate()
const allAlerts = DataStore.getAlerts()
this.recentAlerts = allAlerts
.filter((a: AlertInfo) => a.sessionId === this.sessionId)
.sort((a: AlertInfo, b: AlertInfo) => b.timestamp - a.timestamp)
this.updateAlertStatus()
} catch (error) {
console.error(‘FamilyHomePage loadData failed:’, error)
this.showErrorState()
}
}
showErrorState() {
this.errorMessage = ‘数据加载失败,请返回重试’
}
``
try-catch包裹整个加载流程,任何步骤的异常都会被捕获并展示友好的错误提示,而不是让应用崩溃或显示空白页面。这是医疗应用的基本质量要求——即使出错,也不能让用户陷入"什么也看不到、什么也做不了"的死胡同。
错误状态UI:
``typescript
if (this.errorMessage) {
Column() {
Text(this.errorMessage)
.fontSize(16)
.fontColor(‘#F44336’)
.margin({ bottom: 16 })
Button('重试')
.onClick(() => {
this.errorMessage = ''
this.loadData()
})
}
.width(‘100%’)
.layoutWeight(1)
.justifyContent(FlexAlign.Center)
}
``
错误状态UI提供了"重试"按钮,让用户在遇到错误时可以主动重新加载。这种"自愈"设计比"显示错误然后什么都不做"更友好,也减少了用户对"是不是我的手机有问题"的疑虑。
JSON解析错误处理:
DataStore中的sessions和alerts数据以JSON字符串形式存储,如果数据损坏(如应用在写入过程中崩溃导致JSON不完整),JSON.parse会抛出异常。DataStore的getter方法中应该加入解析保护:
typescript static getSessions(): SessionInfo[] { try { const raw = AppStorage.get<string>('ivguard_sessions') || '[]' return JSON.parse(raw) as SessionInfo[] } catch (e) { console.error('Failed to parse sessions data:', e) return [] } }
返回空数组而非抛出异常,是一种"优雅降级"策略——数据丢失了,但应用不会崩溃。家属会看到空白的信息,可以联系护士确认情况,这比应用崩溃要好得多。
9. 家属端与患者端的差异对比
家属端和患者端虽然共享同一套数据源和部分UI组件,但在功能定位、操作权限和信息展示上存在系统性差异。本章通过详细的对比分析,清晰呈现两端的设计分野。
9.1 核心功能对比
| 特性 | 患者端 | 家属端 |
|---|---|---|
| 液位显示 | LevelProgress(可交互) | LevelProgress(只读) |
| 操作按钮 | 开始/暂停/结束输液 | 联系护士 |
| 预警处理 | 自动通知+手动确认 | 查看记录 |
| 费用管理 | 完整功能 | 不可见 |
| 药物管理 | 添加/删除药品 | 仅查看名称 |
| 导航 | 完整功能 | 仅护士站 |
9.2 详细功能差异分析
液位显示:
患者端和家属端都使用LevelProgress组件展示液位,但绑定方式不同。患者端使用@Link双向绑定,允许用户通过交互影响液位显示(如手动暂停后液位变化停止);家属端使用@Prop单向绑定,只读展示,不能通过交互改变任何状态。
这种差异的根源是角色定位——患者是输液的"操作者",家属是输液的"观察者"。操作者需要能够影响系统状态,观察者只需要获取信息。在UI层面的体现就是数据绑定的方向性——双向vs单向。
操作按钮:
患者端的操作按钮围绕"控制输液过程"设计——开始输液、暂停输液、结束输液。这些操作直接影响硬件设备的状态(如启动/停止滴速传感器)。
家属端的操作按钮围绕"获取帮助"设计——联系护士。家属无法也不应该直接控制输液过程,但他们可以寻求专业人员的帮助。这种"不能做但可以求助"的设计,既尊重了医疗流程的专业性,又给予了家属一定的参与感。
预警处理:
患者端的预警处理是主动的——预警触发时,患者端直接显示预警详情,患者可以确认已知晓。患者还可以设置预警的灵敏度(如调整液位阈值)。
家属端的预警处理是被动的——预警触发后,家属端只是记录和展示,不要求家属确认。家属没有权限调整预警参数,因为这是医疗专业人员的职责。
费用管理:
费用管理是两端差异最大的功能模块。患者端有完整的费用管理功能——查看费用明细、医保报销信息、个人自付金额、历史费用记录等。家属端完全没有费用相关的功能和入口。
这种差异基于两个考虑:
- 隐私保护:费用信息属于患者个人隐私,家属不一定有权查看
- 功能聚焦:家属端的核心功能是监控,费用管理与监控无关,加入后只会增加信息噪声
药物管理:
患者端的药物管理功能允许添加和删除药品——这在输液过程中可能需要(如医生临时增加一种药物)。家属端只显示当前正在输注的药品名称,不能添加或删除。
药品名称的展示也有差异——患者端可能显示更详细的信息(如药品规格、生产厂家、批号等),而家属端只显示通用名称。这种差异遵循了"最小必要信息"原则——家属只需要知道"正在输什么药",不需要了解药品的供应链信息。
导航:
患者端的导航功能是完整的——可以导航到医院的各个区域(门诊、检查室、药房等)。家属端的导航仅限于护士站,因为家属在远程场景中最可能的行动是"联系护士",其他导航功能对他们没有实际价值。
9.3 数据访问权限对比
| 数据项 | 患者端读取 | 患者端写入 | 家属端读取 | 家属端写入 |
|---|---|---|---|---|
| 患者姓名 | 是 | 是 | 是 | 否 |
| 床号 | 是 | 否 | 是 | 否 |
| 液位数据 | 是 | 否 | 是 | 否 |
| 流速数据 | 是 | 否 | 是 | 否 |
| 预警记录 | 是 | 否 | 是 | 否 |
| 会话状态 | 是 | 是 | 是 | 否 |
| 药品列表 | 是 | 是 | 是 | 否 |
| 费用明细 | 是 | 否 | 否 | 否 |
| 医保信息 | 是 | 否 | 否 | 否 |
| 护士排班 | 否 | 否 | 否 | 否 |
| 其他患者数据 | 否 | 否 | 否 | 否 |
这个权限矩阵体现了"角色决定权限"的设计原则:
- 患者端是"操作者",对自身数据有完整的读写权限
- 家属端是"观察者",对所有数据都只有读权限
- 敏感数据(费用、医保)仅对患者端可见
- 护士端内部数据对两端都不可见
9.4 UI组件复用对比
| 组件 | 患者端用法 | 家属端用法 | 复用方式 |
|---|---|---|---|
| LevelProgress | @Link绑定,可交互 | @Prop绑定,只读 | 同组件不同绑定 |
| PatientInfoCard | 完整信息 | 脱敏信息 | 子集复用 |
| AlertItem | 可确认/可操作 | 纯展示 | 去交互复用 |
| NavigationBar | 多入口 | 少入口 | 参数化复用 |
| Button | 控制型(绿/红) | 求助型(橙) | 样式复用 |
组件复用的策略是"同组件不同配置"——同一个组件通过不同的属性绑定和参数配置,在两端呈现不同的行为和外观。这种策略比"两端各自实现"更高效,也比"一个组件包含所有逻辑"更清晰。
LevelProgress的复用设计是组件复用策略的典型代表:
``typescript
// 患者端
LevelProgress({ level: }) // @Link 双向绑定
// 家属端
LevelProgress({ level: this.currentLevel }) // @Prop 单向绑定
``
同一个LevelProgress组件,在患者端通过$level语法实现双向绑定(允许组件内部修改level值),在家属端通过直接传值实现单向绑定(组件内部只能读取level值)。这种差异完全由调用方决定,组件本身不需要知道它是在患者端还是家属端使用。
9.5 信息密度对比
| 维度 | 患者端 | 家属端 |
|---|---|---|
| 首页信息量 | 高(10+信息项) | 低(5-6信息项) |
| 操作按钮数 | 多(4-5个) | 少(1个) |
| 页面数量 | 多(7-8个) | 少(2个) |
| Tab数量 | 多(3-4个) | 无(单页面) |
| 设置项 | 多 | 无 |
家属端的信息密度远低于患者端,这是有意为之的设计选择。家属不需要也不应该被过多的信息淹没——他们的核心需求是"一眼看懂亲人状态",而不是"深入了解输液的所有细节"。
信息密度的心理学依据:
认知负荷理论(Cognitive Load Theory)指出,人的工作记忆容量有限,同时处理过多信息会导致认知过载。在焦虑状态下,人的认知容量进一步缩小。家属端的目标用户——焦急的家属——正是处于认知容量缩小的状态,因此信息密度必须控制在低水平。
“少即是多”(Less is More)在这里不是美学选择,而是可用性选择。每一个额外显示的信息项都在争夺家属有限的注意力,如果一个信息项不是决策必需的,它就应该被隐藏。
9.6 未来差异演进
随着IVGuard产品的演进,家属端和患者端的差异可能会进一步扩大:
家属端可能的未来功能:
- 多患者监控:一位家属同时监控多位亲人的输液状态
- 预约提醒:预约下一次输液时间的提醒
- 健康趋势:查看亲人的输液历史趋势(如过去一周的输液次数)
- 远程支付:为亲人远程支付输液费用(需要患者授权)
- 视频通话:与患者或护士进行视频通话
患者端可能的未来功能:
- 自助服务:自助查询检查结果、预约检查
- 健康教育:输液相关的健康知识推送
- 疼痛评估:输液过程中的疼痛自评
- 满意度评价:对护理服务的评价反馈
这些未来功能将进一步深化两端的差异化设计——家属端向"远程关怀"方向发展,患者端向"自助服务"方向发展。两者共享核心数据(液位、流速、预警),但在功能延伸上各自独立演进。
9.7 差异化设计的工程实现
在工程层面,家属端和患者端的差异化通过以下机制实现:
1. 页面路由隔离:
家属端和患者端使用不同的页面路由,互不交叉。这意味着家属端的代码中不会import患者端的页面组件,反之亦然。
``typescript
// 家属端路由
const familyRoutes: Record<string, object> = {
‘FamilyHomePage’: FamilyHomePage,
‘FamilyAlertPage’: FamilyAlertPage,
‘HospitalNavPage’: HospitalNavPage
}
// 患者端路由
const patientRoutes: Record<string, object> = {
‘PatientHomePage’: PatientHomePage,
‘MedicinePage’: MedicinePage,
‘CostPage’: CostPage,
‘HospitalNavPage’: HospitalNavPage
}
``
HospitalNavPage是两端共享的唯一页面——因为联系护士是两个角色都需要的通用功能。
2. 数据访问层隔离:
家属端通过sanitizeForFamily函数过滤数据,确保只访问允许的数据字段。这个函数作为DataStore和家属端UI之间的中间层,所有数据访问都必须经过这个中间层。
3. 组件配置差异化:
共享组件通过属性参数实现差异化配置。例如LevelProgress组件在患者端传入交互回调,在家属端不传入,组件内部根据回调是否存在来决定是否启用交互功能。
``typescript
@Component
export struct LevelProgress {
@Prop level: number = 0
onLevelClick?: () => void // 可选交互回调
build() {
Stack() {
Progress({ value: this.level, total: 100, type: ProgressType.Ring })
.width(120).height(120)
.color(this.level > 50 ? ‘#4CAF50’ : this.level >= 20 ? ‘#FF9800’ : ‘#F44336’)
Text(${Math.round(this.level)}%)
.fontSize(28).fontWeight(FontWeight.Bold)
}
.onClick(() => {
if (this.onLevelClick) {
this.onLevelClick()
}
})
}
}
``
这种"可选交互"模式让同一个组件可以灵活地适配不同的使用场景,既保持了代码的复用性,又实现了功能的差异化。
附录A:FamilyHomePage完整代码参考
``typescript
@Entry
@Component
struct FamilyHomePage {
@State patientName: string = ‘’
@State bedNumber: string = ‘’
@State currentLevel: number = 100
@State medicineName: string = ‘’
@State flowRate: number = 2.0
@State alertStatus: string = ‘green’
@State recentAlerts: AlertInfo[] = []
@State errorMessage: string = ‘’
private sessionId: string = ‘’
private navController: NavPathStack = new NavPathStack()
aboutToAppear() {
DataStore.init()
this.loadData()
}
loadData() {
try {
const sessions = DataStore.getSessions()
const currentSession = sessions.find(
(s: SessionInfo) => s.status === ‘running’
)
if (currentSession) {
this.patientName = currentSession.patientName
this.bedNumber = currentSession.bedNumber
this.medicineName = currentSession.medicineName
this.sessionId = currentSession.id
}
this.currentLevel = DataStore.getCurrentLevel()
this.flowRate = DataStore.getFlowRate()
const allAlerts = DataStore.getAlerts()
this.recentAlerts = allAlerts
.filter((a: AlertInfo) => a.sessionId === this.sessionId)
.sort((a: AlertInfo, b: AlertInfo) => b.timestamp - a.timestamp)
this.updateAlertStatus()
} catch (error) {
console.error('FamilyHomePage loadData failed:', error)
this.errorMessage = '数据加载失败,请返回重试'
}
}
updateAlertStatus() {
if (this.currentLevel > 50) {
this.alertStatus = ‘green’
} else if (this.currentLevel >= 20) {
this.alertStatus = ‘yellow’
} else {
this.alertStatus = ‘red’
}
}
formatTime(timestamp: number): string {
const date = new Date(timestamp)
const hours = date.getHours().toString().padStart(2, ‘0’)
const minutes = date.getMinutes().toString().padStart(2, ‘0’)
return ${hours}:
}
build() {
Navigation(this.navController) {
Scroll() {
Column({ space: 16 }) {
// 患者信息卡片
Column({ space: 12 }) {
Text(${this.patientName} | 床号)
.fontSize(16).fontWeight(FontWeight.Medium)
Row() {
Text('状态指示: ').fontSize(14)
Row()
.width(16).height(16).borderRadius(8)
.backgroundColor(
this.alertStatus === 'green' ? '#4CAF50' :
this.alertStatus === 'yellow' ? '#FF9800' : '#F44336'
)
}
LevelProgress({ level: this.currentLevel })
Text(${this.medicineName})
.fontSize(14).fontColor('#666666')
Text(流速: ml/min)
.fontSize(13).fontColor('#888888')
}
.width('100%')
.padding(16)
.borderRadius(12)
.backgroundColor(Color.White)
.shadow({ radius: 4, color: '#1F000000', offsetY: 2 })
// 联系护士按钮
Button('联系护士')
.width('80%')
.height(56)
.backgroundColor('#FF9800')
.borderRadius(28)
.fontColor(Color.White)
.fontSize(18)
.fontWeight(FontWeight.Medium)
.onClick(() => {
this.navController.pushPath({ name: 'HospitalNavPage' })
})
// 最近预警摘要
Column({ space: 8 }) {
Text('最近预警')
.fontSize(15).fontWeight(FontWeight.Medium)
if (this.recentAlerts.length === 0) {
Row() {
Text('暂无预警,一切正常')
.fontSize(14).fontColor('#4CAF50')
}
.width('100%')
.padding(16)
.justifyContent(FlexAlign.Center)
} else {
ForEach(
this.recentAlerts.slice(0, 3),
(alert: AlertInfo) => {
Row() {
Row()
.width(8).height(8).borderRadius(4)
.backgroundColor(
alert.severity === 'high' ? '#F44336' : '#FF9800'
)
Text(alert.message)
.fontSize(13).fontColor('#333333')
.layoutWeight(1)
Text(this.formatTime(alert.timestamp))
.fontSize(12).fontColor('#999999')
}
.width('100%')
.padding(12)
.borderRadius(8)
.backgroundColor('#FFF3E0')
}
)
}
}
.width('100%')
.padding(16)
.borderRadius(12)
.backgroundColor(Color.White)
// 导航入口
Row() {
Text('查看全部预警记录')
.fontSize(14).fontColor('#FF9800')
Text(' >')
.fontSize(14).fontColor('#FF9800')
}
.onClick(() => {
this.navController.pushPath({ name: 'FamilyAlertPage' })
})
Row() {
Text('切换到患者端')
.fontSize(13).fontColor('#999999')
}
.onClick(() => {
this.navController.replacePath({ name: 'PatientHomePage' })
})
}
.width('100%')
.padding(16)
}
}
}
}
``
附录B:AlertInfo数据模型
typescript interface AlertInfo { id: string sessionId: string type: 'low_level' | 'flow_anomaly' | 'infusion_complete' | 'device_error' | 'tube_blocked' severity: 'high' | 'normal' message: string timestamp: number }
字段说明:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | string | 预警唯一标识,UUID格式 |
| sessionId | string | 关联的输液会话ID,用于数据隔离 |
| type | string | 预警类型,限定5种取值 |
| severity | string | 严重程度,限定2种取值 |
| message | string | 预警消息,面向用户的中文描述 |
| timestamp | number | 触发时间,Unix毫秒时间戳 |
type字段的取值说明:
- low_level:液位偏低,最常见
- flow_anomaly:流速异常
- infusion_complete:输液完成
- device_error:设备异常
- ube_blocked:管路堵塞
severity字段的取值说明:
- high:高危预警,红色显示,需要立即行动
ormal:常规预警,橙色显示,需要关注
附录C:家属端设计检查清单
以下是家属端UI设计的关键检查项,用于设计和代码评审:
视觉设计
- 绿色指示灯仅在液位>50%时显示
- 黄色指示灯仅在液位20-50%时显示
- 红色指示灯仅在液位<20%时显示
- 状态指示灯尺寸不小于16x16vp
- 联系护士按钮高度不小于56vp
- 关键信息在首屏全部可见
信息脱敏
- 家属端不展示费用信息
- 家属端不展示医保信息
- 家属端不展示其他患者数据
- 家属端不展示护士内部管理数据
- 数据访问经过sanitizeForFamily过滤
情感化设计
- 不使用"危险"等恐吓性词汇
- 不使用"警告"等压迫性词汇
- 空状态有正向引导文案
- 预警分级不超过两级
- 联系护士操作不超过两步
数据加载
- aboutToAppear中执行DataStore.init()
- loadData有try-catch错误处理
- 错误状态提供重试选项
- 预警数据按sessionId过滤
- 预警数据按时间降序排序
交互流程
- 角色切换使用replacePath
- 页面导航使用pushPath
- 预警记录显示时间线结构
- 最后一条预警不显示竖线
- 导航栈不会无限增长
更多推荐




所有评论(0)