基于鸿蒙OS开发静脉输液智能监控系统(20)-多角色路由与导航框架
基于鸿蒙OS开发静脉输液智能监控系统(20)-多角色路由与导航框架
目录
- 1. HarmonyOS 路由方案
- 2. IVGuard 路由架构
- 3. 路由参数设计(RouteParams.ets)
- 4. 页面注册与一致性
- 5. 角色切换机制
- 6. 导航状态管理
- 7. 跨角色共享页面
- 8. 深度链接与跨应用导航(未来)
- 附录 A:完整路由参数接口清单
- 附录 B:路由常见问题 FAQ
- 附录 C:版本演进规划
1. HarmonyOS 路由方案
HarmonyOS 为开发者提供了两套页面路由方案:基于函数式调用的 Router API 和基于声明式容器的 Navigation + NavPathStack。两套方案各有优劣,适用于不同场景。IVGuard 作为一个 MVP 阶段的多角色医疗监护应用,需要在开发效率、代码可维护性、团队学习曲线之间取得平衡。本章节将详细分析两套方案的技术特性,并阐述 IVGuard 选择 Router API 的决策依据。
1.1 Router API(IVGuard 采用)
Router API 是 HarmonyOS 最早提供的页面路由方案,采用函数式调用风格,与传统移动端开发(Android Intent、iOS UIStoryboardSegue)的思路一脉相承。开发者只需导入 @kit.ArkUI 模块中的 router,即可通过简单的函数调用完成页面跳转、返回、替换等操作。
1.1.1 核心方法
router.pushUrl()
最常用的页面跳转方法,将目标页面压入路由栈顶,当前页面保留在栈中。
` ypescript
import { router } from ‘@kit.ArkUI’
router.pushUrl({ url: ‘pages/HomePage’ })
`
pushUrl 支持传入参数对象,目标页面可通过 router.getParams() 获取:
ypescript router.pushUrl({ url: 'pages/MedicineDetailPage', params: { medicineId: 'med_001', fromPage: 'MedicinePage' } as MedicineDetailParams })
pushUrl 还支持 mode 参数,控制跳转行为:
router.RouterMode.Standard(默认):标准模式,每次跳转都会创建新的页面实例并压入栈中router.RouterMode.Single:单例模式,如果栈中已存在目标页面,则将其移至栈顶而非创建新实例
ypescript router.pushUrl({ url: 'pages/MonitorPage', params: { sessionId: 'session_abc' } }, router.RouterMode.Single)
router.back()
返回上一页,将栈顶页面弹出。支持传入可选的 URL 参数,表示返回到指定页面:
ypescript router.back() router.back({ url: 'pages/HomePage' })
router.replaceUrl()
替换当前栈顶页面,不会在栈中新增页面。常用于登录后替换登录页、角色选择后替换选择页等场景,避免用户通过返回键回到不应再访问的页面:
ypescript router.replaceUrl({ url: 'pages/HomePage' })
与 pushUrl 类似,replaceUrl 也支持 mode 参数和 params 传递:
ypescript router.replaceUrl({ url: 'pages/HomePage', params: { role: UserRole.PATIENT } }, router.RouterMode.Standard)
router.clear()
清空路由栈中所有历史页面,仅保留当前页面在栈底。适用于需要彻底重置导航状态的场景,如用户切换角色后:
ypescript router.clear() router.pushUrl({ url: 'pages/HomePage' })
router.getParams()
获取当前页面接收到的路由参数,返回值为 Object 类型,需要通过类型断言转换为具体接口:
ypescript const params = router.getParams() as MedicineDetailParams if (params) { this.medicineId = params.medicineId }
1.1.2 参数传递与接收
Router API 的参数传递采用一次性对象传递模式:调用 pushUrl 时将参数挂载到 params 字段,目标页面在 aboutToAppear 生命周期中通过 router.getParams() 读取。这种模式的关键约束如下:
-
参数序列化:
params对象在传递过程中会经历序列化/反序列化,因此只支持可序列化的数据类型(基本类型、数组、普通对象)。函数、class实例方法、@State装饰的代理对象等不可序列化数据无法正确传递。 -
参数时效性:
router.getParams()返回的参数对象在页面存活期间一直可用,但当页面被replaceUrl替换后,新页面需要重新接收参数。 -
参数类型安全:由于
getParams()返回Object类型,TypeScript 无法在编译期校验参数类型,必须依赖开发者手动断言。IVGuard 通过定义RouteParams.ets中的接口类型来约束参数结构,将类型安全风险降至最低。
ypescript router.pushUrl({ url: 'pages/CostDetailPage', params: { costItemId: 'cost_2024_001' } as CostDetailParams })
` ypescript
@Entry
@Component
struct CostDetailPage {
private costItemId: string = ‘’
aboutToAppear(): void {
const params = router.getParams() as CostDetailParams
if (params && params.costItemId) {
this.costItemId = params.costItemId
this.loadCostDetail()
}
}
private loadCostDetail(): void {
// 根据 costItemId 加载费用详情
}
}
`
1.1.3 路由回调处理
pushUrl 和 replaceUrl 均为异步操作,支持 Promise 回调,开发者可据此处理跳转成功或失败的情况:
ypescript router.pushUrl({ url: 'pages/HomePage' }) .then(() => { console.info('[Router] 跳转成功') }) .catch((err: Error) => { console.error([Router] 跳转失败: ) })
在 IVGuard 中,当前 MVP 阶段未对路由失败做特殊处理,但在生产环境中应增加错误提示和降级逻辑(如跳转失败时显示 Toast 提示用户重试)。
1.1.4 IVGuard 选择 Router API 的理由
IVGuard 选择 Router API 作为路由方案,基于以下考量:
-
学习曲线低:Router API 的函数式调用风格对所有团队成员来说都易于理解和上手,无需额外学习 Navigation 容器的声明式路由概念。在 MVP 阶段,快速迭代比架构完美更重要。
-
代码简洁:每次路由跳转仅需一行函数调用,无需在 Navigation 容器中预注册 NavDestination,代码量显著减少。
-
与多角色分发契合:IVGuard 的 Index 页面根据角色选择分发到不同的 HomePage,这种"一次性分发"模式天然适合
pushUrl,无需复杂的路由拦截逻辑。 -
调试方便:Router API 的路由栈行为直观可预测,开发者可通过
router.getLength()获取栈深度,快速定位导航问题。 -
生态成熟:Router API 是 HarmonyOS 最早的路由方案,文档、示例、社区经验最为丰富,遇到问题更容易找到解决方案。
1.2 Navigation + NavPathStack
Navigation + NavPathStack 是 HarmonyOS 推出的声明式路由方案,以 Navigation 容器为核心,配合 NavDestination 组件和 NavPathStack 路由栈管理类,实现更灵活的页面导航。
1.2.1 Navigation 容器
Navigation 是 ArkUI 提供的导航容器组件,充当页面的根容器,管理其内部的 NavDestination 子页面:
` ypescript
@Entry
@Component
struct MainPage {
private navStack: NavPathStack = new NavPathStack()
build() {
Navigation(this.navStack) {
// 初始内容
}
.mode(NavigationMode.Stack)
.navDestination(this.buildNavDestination)
}
@Builder
buildNavDestination(name: string, param: Object) {
if (name === ‘MonitorPage’) {
MonitorPage()
} else if (name === ‘MedicineDetailPage’) {
MedicineDetailPage({ params: param as MedicineDetailParams })
}
}
}
`
1.2.2 NavPathStack 路由栈
NavPathStack 是 Navigation 方案的核心路由管理类,提供完整的路由栈操作 API:
ypescript this.navStack.pushPath({ name: 'MonitorPage', param: { sessionId: 'abc' } }) this.navStack.pop() this.navStack.replacePath({ name: 'HomePage' }) this.navStack.clear()
NavPathStack 相比 Router API 的路由栈管理具有更细粒度的控制能力:
- 路由拦截:可通过
onWillAppear、onWillShow等回调实现路由拦截,例如权限校验、数据预加载 - 动画自定义:支持自定义页面转场动画,包括入场、出场、共享元素转场
- 路由守卫:可在路由跳转前/后执行逻辑,类似 Vue Router 的 beforeEach/afterEach
- 栈信息查询:可获取栈中所有页面的信息,实现更灵活的导航逻辑
1.2.3 NavDestination 页面注册
Navigation 方案要求每个可导航页面以 NavDestination 组件的形式注册,通过 name 属性标识:
` ypescript
@Component
export struct MonitorPage {
@State sessionId: string = ‘’
build() {
NavDestination() {
Column() {
Text(监护页面 - Session: )
}
}
.title(‘实时监护’)
.onShown(() => {
// 页面显示时的逻辑
})
.onHidden(() => {
// 页面隐藏时的逻辑
})
}
}
`
1.2.4 优势与劣势分析
优势:
-
动画自定义:Navigation 支持丰富的转场动画配置,包括
NavTransition动画对象,可实现 iOS 风格的滑入滑出、Android 风格的淡入淡出、以及自定义共享元素转场动画。对于医疗监护应用,流畅的转场动画能提升用户信任感。 -
路由拦截:通过
Navigation的onNavBarStateChange、onNavigationModeChange以及NavPathStack的事件监听,可以在路由跳转前后执行拦截逻辑。例如在跳转到 MonitorPage 前检查蓝牙连接状态,未连接则拦截并引导用户先配对设备。 -
声明式范式:与 ArkUI 的声明式 UI 范式一致,路由配置和 UI 布局写在同一处,代码内聚性更强。
-
Toolbar/NavBar 自定义:Navigation 容器内置了标题栏、工具栏的配置能力,无需手动实现。
-
路由模式切换:支持
NavigationMode.Stack(栈模式)和NavigationMode.Split(分栏模式),适配不同屏幕尺寸。
劣势:
-
学习曲线较陡:开发者需要理解 Navigation 容器、NavDestination、NavPathStack 三者的关系和协作方式,概念较多。对于团队新成员或外包协作来说,上手成本高于 Router API。
-
页面注册复杂:每个页面都需要以 NavDestination 形式注册,且需要在
navDestinationBuilder 中手动编写条件分发逻辑。当页面数量增多(IVGuard 已有 17+ 个页面)时,维护成本显著上升。 -
嵌套层级深:Navigation 容器包裹所有页面,增加了组件嵌套层级,在某些场景下可能影响布局调试。
-
与 Tabs 组件配合的复杂性:IVGuard 患者端 HomePage 采用 5-Tab 布局,Tabs 组件嵌入 Navigation 容器内的实现方式较为复杂,需要处理 Tab 切换与路由栈的交互。
-
MVP 阶段的过度设计风险:Navigation 方案的诸多高级特性(动画自定义、路由拦截、分栏模式)在 MVP 阶段并非必需,投入学习成本但短期内无法兑现收益。
1.3 两种方案对比
下表从多个维度对 Router API 和 Navigation + NavPathStack 进行系统对比:
| 特性 | Router API | Navigation + NavPathStack |
|---|---|---|
| 使用方式 | 函数调用(router.pushUrl()) |
声明式容器 + 路由栈操作 |
| 页面注册 | main_pages.json 静态配置 |
navDestination Builder 动态注册 |
| 转场动画 | 系统默认,不可自定义 | 完全可自定义(NavTransition) |
| 路由拦截 | 无原生支持 | 支持(onWillShow / onWillAppear) |
| 参数传递 | params 对象(序列化) |
对象引用(可传递不可序列化数据) |
| 路由栈控制 | pushUrl / back / replaceUrl / clear |
pushPath / pop / replacePath / clear |
| 栈深度查询 | router.getLength() |
navStack.size() |
| Tabs 兼容 | 原生友好 | 需额外处理嵌套 |
| 分栏模式 | 不支持 | 支持(NavigationMode.Split) |
| NavBar/Toolbar | 需手动实现 | 内置配置 |
| 学习成本 | 低 | 中 |
| 代码量 | 少 | 多 |
| 适用场景 | 中小型应用、快速迭代 | 大型应用、复杂导航需求 |
| IVGuard 选择 | 采用 | 暂不采用 |
决策总结:IVGuard 在 MVP 阶段选择 Router API,以最低的学习成本和最少的代码量实现多角色路由分发。在后续版本中,若业务需求增长(如需要自定义转场动画、路由拦截鉴权等),可评估迁移至 Navigation 方案。Router API 的 pushUrl 调用与 Navigation 的 pushPath 调用在语义上高度相似,迁移成本可控。
2. IVGuard 路由架构
IVGuard 的路由架构以"角色分发"为核心设计理念:用户在 Index 页面选择角色后,系统根据角色类型跳转到对应的主页面,此后各角色在各自的路由子树中导航。这种"分发-隔离"的设计确保了不同角色的导航逻辑互不干扰,同时通过共享页面机制避免了代码重复。
2.1 Index 入口页角色分发

Index 页面是 IVGuard 应用的入口,也是唯一一个所有角色共享的起始页面。其核心职责是:展示角色选择 UI、持久化用户角色选择、根据选择结果分发到对应角色的主页面。
2.1.1 角色选择与分发实现
` ypescript
import { router } from ‘@kit.ArkUI’
import { DataStore } from ‘…/common/DataStore’
import { UserRole } from ‘…/common/UserRole’
@Entry
@Component
struct Index {
@State currentRole: string = ‘’
aboutToAppear(): void {
const settings = DataStore.loadSettings()
if (settings.role) {
this.currentRole = settings.role
this.navigateToRole(settings.role)
}
}
private selectRole(role: string): void {
this.currentRole = role
const settings = DataStore.loadSettings()
settings.role = role
DataStore.saveSettings(settings)
this.navigateToRole(role)
}
private navigateToRole(role: string): void {
if (role === UserRole.PATIENT) {
this.getUIContext().getRouter().pushUrl({ url: ‘pages/HomePage’ })
} else if (role === UserRole.NURSE) {
this.getUIContext().getRouter().pushUrl({ url: ‘pages/NurseHomePage’ })
} else if (role === UserRole.FAMILY) {
this.getUIContext().getRouter().pushUrl({ url: ‘pages/FamilyHomePage’ })
}
}
build() {
Column() {
Text(‘IVGuard 静脉监护系统’)
.fontSize(28)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 40 })
Button('我是患者')
.width('80%')
.height(60)
.onClick(() => this.selectRole(UserRole.PATIENT))
Button('我是护士')
.width('80%')
.height(60)
.margin({ top: 20 })
.onClick(() => this.selectRole(UserRole.NURSE))
Button('我是家属')
.width('80%')
.height(60)
.margin({ top: 20 })
.onClick(() => this.selectRole(UserRole.FAMILY))
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
}
}
`
2.1.2 角色持久化机制
角色选择的持久化是 IVGuard 用户体验的关键环节。用户首次选择角色后,再次打开应用时应自动跳转到对应角色的主页面,无需重复选择。
持久化流程:
用户点击角色按钮 │ ├─→ selectRole(role) │ ├─→ 更新 @State currentRole │ ├─→ DataStore.loadSettings() → 获取当前设置对象 │ ├─→ settings.role = role → 写入角色字段 │ ├─→ DataStore.saveSettings(settings) → 持久化到 Preferences │ └─→ navigateToRole(role) → 路由跳转 │ └─→ 下次启动应用 ├─→ aboutToAppear() ├─→ DataStore.loadSettings() → 读取持久化设置 ├─→ settings.role 存在? │ ├─→ 是 → navigateToRole(role) → 自动跳转 │ └─→ 否 → 停留在 Index,显示角色选择
DataStore 的底层实现基于 HarmonyOS 的 @kit.ArkData 模块中的 preferences,以键值对形式存储应用设置:
` ypescript
import { preferences } from ‘@kit.ArkData’
export class DataStore {
private static readonly PREF_NAME = ‘ivguard_settings’
private static readonly KEY_SETTINGS = ‘settings’
static loadSettings(): AppSettings {
const pref = preferences.getSync(DataStore.PREF_NAME)
const json = pref.getSync(DataStore.KEY_SETTINGS, ‘{}’) as string
if (json) {
return JSON.parse(json) as AppSettings
}
return { role: ‘’, theme: ‘light’, fontSize: ‘medium’ }
}
static saveSettings(settings: AppSettings): void {
const pref = preferences.getSync(DataStore.PREF_NAME)
pref.putSync(DataStore.KEY_SETTINGS, JSON.stringify(settings))
pref.flush()
}
}
`
2.1.3 角色恢复与自动跳转
aboutToAppear 生命周期是 Index 页面检测持久化角色并执行自动跳转的关键时机。此逻辑在页面 UI 构建之前执行,确保用户不会看到短暂的角色选择界面闪烁:
ypescript aboutToAppear(): void { const settings = DataStore.loadSettings() if (settings.role && settings.role !== '') { this.currentRole = settings.role this.navigateToRole(settings.role) } }
需要注意的是,aboutToAppear 中调用 router.pushUrl 是异步操作,页面仍会短暂渲染 Index 的 UI。在实际体验中,由于跳转速度很快(通常在 100ms 内完成),用户几乎感知不到 Index 页面的闪烁。如果未来需要完全消除闪烁,可以在 Index 页面增加一个加载状态,在角色检测完成前显示 Loading 动画:
` ypescript
@State isCheckingRole: boolean = true
aboutToAppear(): void {
const settings = DataStore.loadSettings()
this.isCheckingRole = false
if (settings.role) {
this.navigateToRole(settings.role)
}
}
build() {
if (this.isCheckingRole) {
LoadingProgress().width(48).height(48)
} else {
// 角色选择 UI
}
}
`
2.1.4 getUIContext().getRouter() 与直接导入 router 的区别
在 IVGuard 的 Index 页面中,路由跳转使用的是 this.getUIContext().getRouter() 而非直接导入的 router 模块。这两种方式的区别如下:
- 直接导入:
import { router } from '@kit.ArkUI',直接使用router.pushUrl(),适用于非@Entry组件或无法获取 UIContext 的场景。 - UIContext 获取:
this.getUIContext().getRouter(),通过组件的 UIContext 上下文获取路由实例,适用于@Entry组件内部,确保路由操作与当前页面的 UI 上下文绑定。
在 @Entry 组件中,推荐使用 getUIContext().getRouter(),因为它能确保路由操作在正确的页面上下文中执行,避免在多实例场景下出现路由混乱。但在 IVGuard 当前的单实例 MVP 架构中,两种方式的效果完全一致,开发者可根据习惯选择。
2.2 患者端路由图
患者端是 IVGuard 功能最丰富的角色端,包含实时监护、用药管理、历史记录、数据分析、费用管理五大功能模块,以及监护详情、院内导航等扩展页面。
2.2.1 完整路由图
┌─────────────────────────────────────────────────────────────┐ │ Index (角色选择) │ │ │ │ │ selectRole(PATIENT) │ │ │ │ │ ▼ │ │ ┌─────── HomePage (5-Tab) ───────┐ │ │ │ Tab0 Tab1 Tab2 Tab3 Tab4 │ │ │ │ 监护概览 用药 历史 分析 费用 │ │ │ └──┬───────┬──────┬─────┬─────┬───┘ │ │ │ │ │ │ │ │ │ ┌────────────┘ │ │ │ │ │ │ │ (内嵌在Tab0) │ │ │ │ │ │ ▼ ▼ ▼ ▼ ▼ │ │ monitorTab MedicinePage HistoryPage AnalysisPage CostPage │ (组件内嵌) │ │ │ │ │ ▼ │ │ │ │ MedicineDetailPage │ │ │ │ (pushUrl + medicineId) │ │ │ │ │ │ │ │ ┌─────────────────────────────────────────────┘ │ │ │ │ (从 Tab0 / MonitorPage 可跳转) │ │ │ ▼ │ │ │ MonitorPage (pushUrl + sessionId) │ │ │ │ │ │ │ └──→ HospitalNavPage (院内导航) │ │ │ │ │ │ ┌────────────┘ │ │ ▼ ▼ │ InsuranceSetupPage CostDetailPage │ (pushUrl) (pushUrl + costItemId) │ │ │ ┌─────────────────────────────────────────────────────────┘ │ │ (从任意页面可跳转) │ ▼ │ SettingsPage (pushUrl) └─────────────────────────────────────────────────────────────┘
2.2.2 患者端路由详解
Tab0 - 监护概览(monitorTab)
监护概览是 HomePage 的第一个 Tab,以组件内嵌方式直接在 TabContent 中展示,不涉及路由跳转。用户在此 Tab 可查看当前输液状态、滴速、剩余时间等关键信息。当用户需要查看更详细的监护数据或操作设备时,点击"查看详情"按钮跳转到 MonitorPage:
ypescript TabContent() { MonitorTab({ onDetailClick: () => { router.pushUrl({ url: 'pages/MonitorPage', params: { sessionId: this.currentSessionId } as MonitorParams }) }}) } .tab('监护')
Tab1 - 用药管理(MedicinePage)
点击用药 Tab 时,通过 onChange 回调触发 router.pushUrl 跳转到 MedicinePage:
ypescript Tabs({ index: this.currentTab }) { TabContent() { // 占位内容 } .tab('用药') } .onChange((index: number) => { if (index === 1) { router.pushUrl({ url: 'pages/MedicinePage' }) } })
MedicinePage 展示用药列表,点击某条用药记录可跳转到详情页:
ypescript List() { ForEach(this.medicineList, (item: MedicineInfo) => { ListItem() { MedicineCard({ medicine: item }) .onClick(() => { router.pushUrl({ url: 'pages/MedicineDetailPage', params: { medicineId: item.id } as MedicineDetailParams }) }) } }) }
Tab2 - 历史记录(HistoryPage)
历史记录页面展示患者的历史输液记录,以只读列表形式呈现,当前版本不支持从历史记录跳转到详情页(未来可扩展):
ypescript .onChange((index: number) => { if (index === 2) { router.pushUrl({ url: 'pages/HistoryPage' }) } })
Tab3 - 数据分析(AnalysisPage)
数据分析页面展示输液数据的统计图表,当前版本为独立页面,无需跳转到子页面:
ypescript .onChange((index: number) => { if (index === 3) { router.pushUrl({ url: 'pages/AnalysisPage' }) } })
Tab4 - 费用管理(CostPage)
费用管理页面是患者端路由深度最深的模块,包含费用列表、费用详情、医保配置三个层级的页面:
ypescript .onChange((index: number) => { if (index === 4) { router.pushUrl({ url: 'pages/CostPage' }) } })
CostPage 中的跳转:
` ypescript
Column() {
List() {
ForEach(this.costList, (item: CostItem) => {
ListItem() {
CostCard({ cost: item })
.onClick(() => {
router.pushUrl({
url: ‘pages/CostDetailPage’,
params: { costItemId: item.id } as CostDetailParams
})
})
}
})
}
Button(‘医保配置’)
.onClick(() => {
router.pushUrl({ url: ‘pages/InsuranceSetupPage’ })
})
}
`
通用页面跳转
以下页面可从患者端多个位置跳转:
- MonitorPage:从 HomePage 的 Tab0 "查看详情"按钮或监护告警通知触发
- HospitalNavPage:从 MonitorPage 的"院内导航"按钮或 HomePage 的快捷入口触发
- SettingsPage:从任意页面的设置图标触发
2.2.3 患者端路由栈示例
假设用户按以下顺序操作:选择患者角色 -> 点击用药 Tab -> 点击某条用药记录 -> 返回 -> 点击费用 Tab -> 点击费用详情 -> 返回。路由栈变化如下:
操作 路由栈(从底到顶) ───────────── ──────────────────────────────── 选择患者角色 [Index, HomePage] 点击用药Tab [Index, HomePage, MedicinePage] 点击用药记录 [Index, HomePage, MedicinePage, MedicineDetailPage] 返回 [Index, HomePage, MedicinePage] 返回 [Index, HomePage] 点击费用Tab [Index, HomePage, CostPage] 点击费用详情 [Index, HomePage, CostPage, CostDetailPage] 返回 [Index, HomePage, CostPage]
可以看到,由于 Tab 切换使用 pushUrl 而非 replaceUrl,每次 Tab 切换都会在栈中新增页面。这导致栈中可能出现 HomePage -> MedicinePage -> CostPage 这样的序列,用户需要多次按返回键才能回到 HomePage。这是当前 MVP 版本的已知问题,将在后续版本中优化(详见第 6.2 节)。
2.3 护士端路由图
护士端的路由结构相对简洁,核心功能围绕患者列表和数据分析展开。
2.3.1 完整路由图
┌─────────────────────────────────────────────────────────┐ │ Index (角色选择) │ │ │ │ │ selectRole(NURSE) │ │ │ │ │ ▼ │ │ ┌──── NurseHomePage ────┐ │ │ │ 患者列表 │ 数据分析 │ │ │ └────┬──────────┬───────┘ │ │ │ │ │ │ ▼ ▼ │ │ PatientListPage NurseAnalysisPage │ │ (pushUrl + (pushUrl) │ │ patientName, │ │ bedNumber) │ │ │ │ ┌────────────────────────────┐ │ │ │ (从任意页面可跳转) │ │ │ ▼ │ │ │ SettingsPage (pushUrl) │ │ └─────────────────────────────────────────────────────────┘
2.3.2 护士端路由详解
NurseHomePage
护士端主页采用简单的按钮布局,提供两个主要功能入口:
` ypescript
@Entry
@Component
struct NurseHomePage {
build() {
Column() {
Text(‘护士工作台’)
.fontSize(24)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 30 })
Button('患者列表')
.width('80%')
.height(60)
.onClick(() => {
router.pushUrl({ url: 'pages/PatientListPage' })
})
Button('数据分析')
.width('80%')
.height(60)
.margin({ top: 20 })
.onClick(() => {
router.pushUrl({ url: 'pages/NurseAnalysisPage' })
})
Image(('app.media.ic_settings'))
.width(28)
.height(28)
.onClick(() => {
router.pushUrl({ url: 'pages/SettingsPage' })
})
}
.width('100%')
.height('100%')
}
}
`
PatientListPage
患者列表页展示护士负责的所有患者,跳转时携带 patientName 和 bedNumber 参数:
ypescript router.pushUrl({ url: 'pages/PatientListPage', params: { patientName: '张三', bedNumber: 'A-301' } as NursePatientParams })
NurseAnalysisPage
数据分析页展示护士所管辖患者的汇总统计数据,当前版本为独立展示页,无子页面跳转。
2.3.3 护士端路由栈示例
操作 路由栈(从底到顶) ───────────── ──────────────────────────────── 选择护士角色 [Index, NurseHomePage] 点击患者列表 [Index, NurseHomePage, PatientListPage] 返回 [Index, NurseHomePage] 点击数据分析 [Index, NurseHomePage, NurseAnalysisPage]
护士端路由栈深度较浅,最多三层,导航逻辑简单明了。

2.4 家属端路由图
家属端的路由结构同样简洁,核心功能围绕告警通知和院内联系展开。
2.4.1 完整路由图
┌─────────────────────────────────────────────────────────┐ │ Index (角色选择) │ │ │ │ │ selectRole(FAMILY) │ │ │ │ │ ▼ │ │ ┌──── FamilyHomePage ────┐ │ │ │ 告警通知 │ 联系护士 │ │ │ └────┬──────────┬───────┘ │ │ │ │ │ │ ▼ ▼ │ │ FamilyAlertPage HospitalNavPage │ │ (pushUrl + (pushUrl, │ │ patientId, 联系护士模式) │ │ patientName) │ │ │ │ ┌────────────────────────────┐ │ │ │ (从任意页面可跳转) │ │ │ ▼ │ │ │ SettingsPage (pushUrl) │ │ └─────────────────────────────────────────────────────────┘
2.4.2 家属端路由详解
FamilyHomePage
家属端主页提供告警通知和联系护士两个功能入口:
` ypescript
@Entry
@Component
struct FamilyHomePage {
private patientId: string = ‘’
private patientName: string = ‘’
aboutToAppear(): void {
const settings = DataStore.loadSettings()
this.patientId = settings.patientId || ‘’
this.patientName = settings.patientName || ‘’
}
build() {
Column() {
Text(‘家属监护面板’)
.fontSize(24)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 30 })
Button('告警通知')
.width('80%')
.height(60)
.onClick(() => {
router.pushUrl({
url: 'pages/FamilyAlertPage',
params: { patientId: this.patientId, patientName: this.patientName } as FamilyPatientParams
})
})
Button('联系护士')
.width('80%')
.height(60)
.margin({ top: 20 })
.onClick(() => {
router.pushUrl({ url: 'pages/HospitalNavPage' })
})
Image(('app.media.ic_settings'))
.width(28)
.height(28)
.onClick(() => {
router.pushUrl({ url: 'pages/SettingsPage' })
})
}
.width('100%')
.height('100%')
}
}
`
FamilyAlertPage
告警通知页展示家属关注的患者的告警历史,跳转时携带 patientId 和 patientName 以便加载对应患者的告警数据:
` ypescript
@Entry
@Component
struct FamilyAlertPage {
@State patientId: string = ‘’
@State patientName: string = ‘’
@State alertList: AlertInfo[] = []
aboutToAppear(): void {
const params = router.getParams() as FamilyPatientParams
if (params) {
this.patientId = params.patientId
this.patientName = params.patientName
}
this.loadAlerts()
}
private loadAlerts(): void {
// 根据 patientId 加载告警数据
}
build() {
Column() {
Text(${this.patientName} 的告警通知)
.fontSize(20)
.fontWeight(FontWeight.Bold)
List() {
ForEach(this.alertList, (alert: AlertInfo) => {
ListItem() {
AlertCard({ alert: alert })
}
})
}
}
}
}
`
HospitalNavPage(家属端)
家属端的 HospitalNavPage 用于联系护士,与患者端的 HospitalNavPage 共享同一页面文件,但入口参数和显示内容有所差异(详见第 7 节)。
2.4.3 家属端路由栈示例
操作 路由栈(从底到顶) ───────────── ──────────────────────────────── 选择家属角色 [Index, FamilyHomePage] 点击告警通知 [Index, FamilyHomePage, FamilyAlertPage] 返回 [Index, FamilyHomePage] 点击联系护士 [Index, FamilyHomePage, HospitalNavPage]
2.4.4 三角色路由对比总览
| 维度 | 患者端 | 护士端 | 家属端 |
|---|---|---|---|
| 入口页 | HomePage (5-Tab) | NurseHomePage | FamilyHomePage |
| 页面数量 | 10 | 3 | 3 |
| 最大栈深度 | 5 | 3 | 3 |
| 有参数页面 | 3 | 1 | 1 |
| 共享页面 | SettingsPage, HospitalNavPage | SettingsPage | SettingsPage, HospitalNavPage |
| Tab导航 | 5个Tab | 无 | 无 |
| 子页面最深路径 | CostPage -> CostDetailPage | PatientListPage | FamilyAlertPage |
3. 路由参数设计(RouteParams.ets)
路由参数是页面间数据传递的核心机制。IVGuard 将所有路由参数接口集中定义在 RouteParams.ets 文件中,确保参数结构的类型安全、可追溯、易维护。这种"参数接口集中管理"的设计模式在多人协作中尤为重要——当某个页面的参数结构变更时,只需修改 RouteParams.ets 中的接口定义,编译器即可在所有调用点报错,避免遗漏。
3.1 接口定义
RouteParams.ets 位于 common/ 目录下,与 DataStore、UserRole 等公共模块并列:
common/ ├─ DataStore.ets ├─ UserRole.ets ├─ RouteParams.ets ← 路由参数接口 ├─ Constants.ets └─ Logger.ets
完整的接口定义如下:
` ypescript
export interface MonitorParams {
sessionId: string
}
export interface MedicineDetailParams {
medicineId: string
}
export interface CostDetailParams {
costItemId: string
}
export interface NursePatientParams {
patientName: string
bedNumber: string
}
export interface FamilyPatientParams {
patientId: string
patientName: string
}
`
每个接口的设计遵循以下原则:
-
接口命名规范:
{PageName}Params,其中 PageName 为目标页面的名称去掉 “Page” 后缀。例如MedicineDetailParams对应MedicineDetailPage。 -
字段最小化:每个接口只包含目标页面必需的参数,不传递冗余数据。例如
MedicineDetailParams只传medicineId,而非整个MedicineInfo对象,详情页自行根据 ID 查询完整数据。 -
基本类型优先:参数字段只使用
string、number、boolean等基本类型,避免传递复杂对象。这既符合 Router API 的序列化约束,也降低了页面间的数据耦合度。 -
只传 ID,不传对象:这是 IVGuard 路由参数设计的核心原则。传递 ID 而非完整对象有以下优势:
- 避免序列化/反序列化导致的数据丢失(如
Date对象会变为字符串) - 目标页面总是获取最新数据,而非跳转时的快照
- 参数接口稳定,不随业务对象字段变更而频繁修改
- 避免序列化/反序列化导致的数据丢失(如
设计权衡:传 ID 还是传对象?
| 方案 | 优势 | 劣势 | 适用场景 |
|---|---|---|---|
| 只传 ID | 参数稳定、数据最新、解耦 | 目标页需额外请求数据 | 数据可能变化的场景 |
| 传完整对象 | 目标页无需请求、响应快 | 参数易变、耦合度高 | 数据不变的静态场景 |
| 混合方案 | 灵活 | 复杂度高 | 复杂业务场景 |
IVGuard 选择"只传 ID"方案,原因如下:
- 医疗数据具有时效性,患者费用、用药信息随时可能更新,目标页面应获取最新数据
- 网络延迟在医疗场景下可接受(通常 < 500ms),且可在目标页面展示 Loading 状态
- 保持参数接口的稳定性,避免因业务对象字段增删而频繁修改路由参数
3.2 参数传递
参数传递统一通过 router.pushUrl 的 params 字段实现,并使用 as 类型断言确保类型安全:
` ypescript
import { router } from ‘@kit.ArkUI’
import { MonitorParams, MedicineDetailParams, CostDetailParams } from ‘…/common/RouteParams’
router.pushUrl({
url: ‘pages/MedicineDetailPage’,
params: { medicineId: ‘123’ } as MedicineDetailParams
})
`
对于无参数的页面跳转,直接省略 params:
ypescript router.pushUrl({ url: 'pages/MedicinePage' })
参数传递的注意事项:
-
参数必须可序列化:Router API 会对
params对象进行序列化处理,因此参数值只能是基本类型、数组、普通对象。以下类型不可传递:- 函数(
Function) class实例(包含方法的类实例)@State/@Prop装饰的代理对象Date对象(会变为字符串)undefined(会变为null)
- 函数(
-
参数大小限制:虽然 HarmonyOS 未明确限制参数大小,但考虑到序列化性能和内存占用,建议单个参数对象不超过 1KB。对于大数据传递,应使用 DataStore 等全局状态管理方案。
-
参数时机:
params只能在pushUrl/replaceUrl时传递,back()不支持携带参数。如果需要在返回时传递数据,应使用 AppStorage 或 DataStore 等全局状态机制。 -
类型断言的安全性:
as类型断言是编译期行为,运行时不会进行类型检查。如果传递的参数结构与接口定义不一致,不会报错但可能导致目标页面行为异常。因此,参数传递代码应严格遵循 RouteParams.ets 中定义的接口结构。
3.3 参数接收
目标页面在 aboutToAppear 生命周期中通过 router.getParams() 接收参数,并使用与 RouteParams.ets 中定义的对应接口进行类型断言:
` ypescript
import { router } from ‘@kit.ArkUI’
import { MedicineDetailParams } from ‘…/common/RouteParams’
@Entry
@Component
struct MedicineDetailPage {
@State medicineId: string = ‘’
@State medicineDetail: MedicineInfo | null = null
aboutToAppear(): void {
const params = router.getParams() as MedicineDetailParams
if (params && params.medicineId) {
this.medicineId = params.medicineId
this.loadMedicineDetail()
}
}
private async loadMedicineDetail(): Promise {
this.medicineDetail = await MedicineService.getDetail(this.medicineId)
}
build() {
Column() {
if (this.medicineDetail) {
Text(this.medicineDetail.name).fontSize(22)
Text(用法用量: ).fontSize(16)
} else {
LoadingProgress().width(48).height(48)
}
}
.width(‘100%’)
.height(‘100%’)
}
}
`
参数接收的安全模式:
由于 router.getParams() 可能返回 undefined(如用户直接打开页面、参数丢失等),接收参数时必须进行空值检查:
` ypescript
const params = router.getParams() as MedicineDetailParams
if (params && params.medicineId) {
this.medicineId = params.medicineId
} else {
this.medicineId = ‘’
Logger.warn(‘MedicineDetailPage’, ‘缺少必要参数 medicineId’)
}
`
更健壮的参数校验模式:
` ypescript
aboutToAppear(): void {
const params = router.getParams()
if (!params) {
Logger.error(‘MedicineDetailPage’, ‘路由参数为空’)
router.back()
return
}
const typedParams = params as MedicineDetailParams
if (!typedParams.medicineId) {
Logger.error(‘MedicineDetailPage’, ‘缺少 medicineId 参数’)
router.back()
return
}
this.medicineId = typedParams.medicineId
this.loadMedicineDetail()
}
`
参数接收时机:
router.getParams() 在页面的任何生命周期中都可以调用,但推荐在 aboutToAppear 中一次性获取并保存到组件状态中,避免在后续逻辑中重复调用。这是因为:
aboutToAppear在页面构建前调用,参数获取后可立即用于数据加载- 将参数保存到
@State变量后,UI 可自动响应数据变化 - 避免在
build方法中调用getParams(),因为build可能被多次调用
各角色端的参数接收模式总结:
| 页面 | 参数接口 | 接收时机 | 数据加载方式 |
|---|---|---|---|
| MonitorPage | MonitorParams | aboutToAppear | 根据 sessionId 查询监护数据 |
| MedicineDetailPage | MedicineDetailParams | aboutToAppear | 根据 medicineId 查询用药详情 |
| CostDetailPage | CostDetailParams | aboutToAppear | 根据 costItemId 查询费用详情 |
| PatientListPage | NursePatientParams | aboutToAppear | 根据 patientName+bedNumber 筛选 |
| FamilyAlertPage | FamilyPatientParams | aboutToAppear | 根据 patientId 查询告警列表 |
| MedicinePage | 无 | aboutToAppear | 加载当前患者的全部用药 |
| HistoryPage | 无 | aboutToAppear | 加载当前患者的历史记录 |
| AnalysisPage | 无 | aboutToAppear | 加载当前患者的分析数据 |
| CostPage | 无 | aboutToAppear | 加载当前患者的费用列表 |
| SettingsPage | 无 | aboutToAppear | 加载当前设置 |
| HospitalNavPage | 无 | aboutToAppear | 根据角色加载导航/联系信息 |
4. 页面注册与一致性
HarmonyOS 的 Router API 要求所有可路由页面必须在 main_pages.json 中注册。这个看似简单的配置文件,却是 IVGuard 多角色路由架构中最容易出现一致性问题的环节。页面注册与页面文件的不一致是导致白屏、编译错误等问题的首要原因,需要严格维护。
4.1 main_pages.json 维护
main_pages.json 位于 src/main/resources/base/profile/ 目录下,是一个 JSON 数组,列出所有可路由页面的路径:
json [ "pages/Index", "pages/HomePage", "pages/MonitorPage", "pages/MedicinePage", "pages/MedicineDetailPage", "pages/HistoryPage", "pages/AnalysisPage", "pages/CostPage", "pages/CostDetailPage", "pages/InsuranceSetupPage", "pages/SettingsPage", "pages/HospitalNavPage", "pages/NurseHomePage", "pages/PatientListPage", "pages/NurseAnalysisPage", "pages/FamilyHomePage", "pages/FamilyAlertPage" ]
注册规则:
- 路径格式:页面路径以
pages/开头,不含文件扩展名(.ets省略)。 - 首项必须为 Index:数组的第一个元素必须是应用的入口页面(通常为
pages/Index)。 - 顺序无关:除首项外,其余页面的注册顺序不影响路由功能,但建议按功能模块分组排列以便维护。
- 唯一性:每个页面路径只能出现一次,重复注册会导致编译警告。
按角色分组的推荐排列方式:
`json
[
“pages/Index”,
“pages/HomePage”,
“pages/MonitorPage”,
“pages/MedicinePage”,
“pages/MedicineDetailPage”,
“pages/HistoryPage”,
“pages/AnalysisPage”,
“pages/CostPage”,
“pages/CostDetailPage”,
“pages/InsuranceSetupPage”,
“pages/NurseHomePage”,
“pages/PatientListPage”,
“pages/NurseAnalysisPage”,
“pages/FamilyHomePage”,
“pages/FamilyAlertPage”,
“pages/SettingsPage”,
“pages/HospitalNavPage”
]
`
这种分组方式将共享页面(SettingsPage、HospitalNavPage)放在最后,各角色的专属页面按角色分组,便于新成员快速了解页面归属。
新增页面的标准流程:
每当需要在 IVGuard 中新增一个页面,开发者必须完成以下步骤:
`
步骤 1: 创建页面文件
→ 在 pages/ 目录下创建 XxxPage.ets 文件
→ 实现 @Entry @Component struct XxxPage { … }
步骤 2: 注册页面路径
→ 在 main_pages.json 中添加 “pages/XxxPage”
→ 注意路径大小写与文件名一致
步骤 3: 定义路由参数(如需)
→ 在 RouteParams.ets 中添加 XxxParams 接口
步骤 4: 实现跳转逻辑
→ 在源页面中调用 router.pushUrl({ url: ‘pages/XxxPage’, params: {…} })
步骤 5: 验证
→ 编译运行,确认跳转正常,无白屏
`
4.2 白屏问题排查
白屏是 HarmonyOS 应用开发中最常见的路由问题之一,通常由页面注册与页面文件不一致导致。以下三种典型场景需要特别注意:
4.2.1 场景一:页面文件存在但未注册
现象:调用 router.pushUrl({ url: 'pages/SomePage' }) 后,当前页面消失,目标页面显示白屏,无任何 UI 内容。
原因:页面文件 SomePage.ets 存在于 pages/ 目录中,但未在 main_pages.json 中注册。Router API 找不到注册信息,无法正确加载页面。
排查步骤:
`
-
确认页面文件路径
→ 检查 pages/SomePage.ets 是否存在 -
检查 main_pages.json
→ 搜索 “pages/SomePage” 是否在注册列表中 -
对比路径大小写
→ “pages/SomePage” vs “pages/somepage” 是否一致 -
添加注册并重新编译
→ 在 main_pages.json 中添加 “pages/SomePage”
`
4.2.2 场景二:页面注册但文件不存在
现象:编译阶段报错,提示找不到页面文件。
原因:main_pages.json 中注册了某个页面路径,但对应的 .ets 文件不存在。
错误信息示例:
Error: Cannot find module 'pages/NonExistentPage'
排查步骤:
`
-
检查编译错误信息
→ 确认哪个页面路径报错 -
确认页面文件是否遗漏
→ 检查 pages/ 目录下是否存在对应文件 -
如果文件确实不需要,从 main_pages.json 中移除注册
→ 删除对应的注册条目
`
4.2.3 场景三:路径大小写不一致
现象:调用 router.pushUrl({ url: 'pages/monitorPage' }) 后白屏。
原因:页面文件名为 MonitorPage.ets,注册路径为 pages/MonitorPage,但跳转时写成了 pages/monitorPage(小写 m)。HarmonyOS 的页面路径是大小写敏感的,路径不匹配会导致白屏。
排查步骤:
`
-
对比三处路径
→ 文件名: MonitorPage.ets
→ main_pages.json: “pages/MonitorPage”
→ pushUrl 调用: ‘pages/monitorPage’ ← 这里不一致! -
统一路径命名
→ 确保所有页面路径使用 PascalCase 命名 -
建立路径常量
→ 在 Constants.ets 中定义路径常量,避免手写路径
`
路径常量方案:
为彻底避免路径手写导致的不一致问题,IVGuard 推荐在 Constants.ets 中定义页面路径常量:
ypescript export class PageRoutes { static readonly INDEX = 'pages/Index' static readonly HOME = 'pages/HomePage' static readonly MONITOR = 'pages/MonitorPage' static readonly MEDICINE = 'pages/MedicinePage' static readonly MEDICINE_DETAIL = 'pages/MedicineDetailPage' static readonly HISTORY = 'pages/HistoryPage' static readonly ANALYSIS = 'pages/AnalysisPage' static readonly COST = 'pages/CostPage' static readonly COST_DETAIL = 'pages/CostDetailPage' static readonly INSURANCE_SETUP = 'pages/InsuranceSetupPage' static readonly SETTINGS = 'pages/SettingsPage' static readonly HOSPITAL_NAV = 'pages/HospitalNavPage' static readonly NURSE_HOME = 'pages/NurseHomePage' static readonly PATIENT_LIST = 'pages/PatientListPage' static readonly NURSE_ANALYSIS = 'pages/NurseAnalysisPage' static readonly FAMILY_HOME = 'pages/FamilyHomePage' static readonly FAMILY_ALERT = 'pages/FamilyAlertPage' }
使用常量后的跳转代码:
ypescript router.pushUrl({ url: PageRoutes.MONITOR }) router.pushUrl({ url: PageRoutes.MEDICINE_DETAIL, params: { medicineId: item.id } as MedicineDetailParams })
这种方式将路径字符串集中管理,既避免了手写错误,也方便全局重命名页面路径。
4.2.4 白屏排查决策树
白屏现象 │ ├─→ 编译是否通过? │ ├─→ 否 → 页面注册但文件不存在 → 检查文件是否遗漏或移除注册 │ └─→ 是 → 继续排查 │ ├─→ main_pages.json 是否注册? │ ├─→ 否 → 文件存在但未注册 → 添加注册 │ └─→ 是 → 继续排查 │ ├─→ pushUrl 路径与注册路径是否一致(含大小写)? │ ├─→ 否 → 路径不一致 → 修正路径 │ └─→ 是 → 继续排查 │ ├─→ 页面 aboutToAppear 是否有未捕获异常? │ ├─→ 是 → 生命周期异常 → 修复异常代码 │ └─→ 否 → 继续排查 │ └─→ 页面 build 方法是否正确? ├─→ 否 → UI 构建错误 → 修复 build 逻辑 └─→ 是 → 深入调试(检查 hilog 输出)
4.2.5 白屏问题预防措施
为从源头预防白屏问题,IVGuard 建议在开发流程中引入以下措施:
- CI 脚本校验:在持续集成流水线中增加脚本,自动检查
main_pages.json中的注册路径与pages/目录下的文件是否一一对应:
` ypescript
// check-pages.js (CI 脚本示意)
const fs = require(‘fs’)
const path = require(‘path’)
const pagesDir = path.join(__dirname, ‘src/main/ets/pages’)
const registeredPages = JSON.parse(
fs.readFileSync(path.join(__dirname, ‘src/main/resources/base/profile/main_pages.json’), ‘utf-8’)
)
const actualPages = fs.readdirSync(pagesDir)
.filter(f => f.endsWith(‘.ets’))
.map(f => ‘pages/’ + f.replace(‘.ets’, ‘’))
const missing = actualPages.filter(p => !registeredPages.includes§)
const extra = registeredPages.filter(p => !actualPages.includes§)
if (missing.length > 0) {
console.error(未注册的页面: )
}
if (extra.length > 0) {
console.error(注册但文件不存在的页面: )
}
`
-
Code Review 检查清单:在代码审查时,凡是涉及新增/删除页面的 PR,必须确认
main_pages.json同步更新。 -
路径常量强制使用:团队约定所有路由跳转必须使用
PageRoutes常量,禁止硬编码页面路径字符串。
5. 角色切换机制
IVGuard 允许用户在应用运行期间切换角色,这一功能主要通过 SettingsPage 实现。角色切换涉及导航状态重置、数据源切换、UI 重建等复杂操作,需要精心设计以确保用户体验流畅。
5.1 角色切换的触发入口
角色切换的唯一入口是 SettingsPage 中的"切换角色"功能。SettingsPage 是三角色共享页面,任何角色的用户都可以通过设置页面发起角色切换。
` ypescript
@Entry
@Component
struct SettingsPage {
@State currentRole: string = ‘’
aboutToAppear(): void {
const settings = DataStore.loadSettings()
this.currentRole = settings.role || ‘’
}
private switchRole(newRole: string): void {
const settings = DataStore.loadSettings()
settings.role = newRole
DataStore.saveSettings(settings)
router.clear()
if (newRole === UserRole.PATIENT) {
router.pushUrl({ url: 'pages/HomePage' })
} else if (newRole === UserRole.NURSE) {
router.pushUrl({ url: 'pages/NurseHomePage' })
} else if (newRole === UserRole.FAMILY) {
router.pushUrl({ url: 'pages/FamilyHomePage' })
}
}
build() {
Column() {
Text(‘设置’)
.fontSize(24)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 20 })
Text(当前角色: )
.fontSize(16)
.margin({ bottom: 30 })
Button('切换为患者')
.width('80%')
.height(50)
.onClick(() => this.switchRole(UserRole.PATIENT))
Button('切换为护士')
.width('80%')
.height(50)
.margin({ top: 15 })
.onClick(() => this.switchRole(UserRole.NURSE))
Button('切换为家属')
.width('80%')
.height(50)
.margin({ top: 15 })
.onClick(() => this.switchRole(UserRole.FAMILY))
Button('返回角色选择')
.width('80%')
.height(50)
.margin({ top: 30 })
.onClick(() => {
router.clear()
router.pushUrl({ url: 'pages/Index' })
})
}
.width('100%')
.height('100%')
.padding(20)
}
}
`
5.2 角色切换的导航状态重置
角色切换时必须彻底重置导航状态,否则用户可能在护士端的页面栈中按返回键回到患者端的页面,造成逻辑混乱。IVGuard 采用 router.clear() + router.pushUrl() 的两步操作实现状态重置:
ypescript router.clear() // 清空所有历史页面 router.pushUrl({ url: 'pages/HomePage' }) // 新角色首页入栈
执行后的路由栈:
`
切换前: [Index, NurseHomePage, PatientListPage, SettingsPage]
router.clear() → [SettingsPage](仅保留当前页)
router.pushUrl → [SettingsPage, HomePage]
// 注:clear() 后当前页仍在栈中,pushUrl 新页面后,用户按返回会回到 SettingsPage
// 如果需要彻底隔离,应使用 replaceUrl:
router.clear()
router.replaceUrl({ url: ‘pages/HomePage’ }) // 替换当前页
// 栈: [HomePage]
`
更严谨的角色切换流程:
` ypescript
private switchRole(newRole: string): void {
const settings = DataStore.loadSettings()
settings.role = newRole
DataStore.saveSettings(settings)
const targetUrl = this.getRoleHomePage(newRole)
router.clear()
router.replaceUrl({ url: targetUrl })
}
private getRoleHomePage(role: string): string {
if (role === UserRole.PATIENT) return ‘pages/HomePage’
if (role === UserRole.NURSE) return ‘pages/NurseHomePage’
if (role === UserRole.FAMILY) return ‘pages/FamilyHomePage’
return ‘pages/Index’
}
`
这样切换后路由栈中只有新角色的首页,用户按返回键不会回到旧角色的页面。
角色切换的完整时序图:
用户点击"切换为患者" │ ▼ switchRole(PATIENT) │ ┌───────────┼───────────┐ ▼ ▼ ▼ DataStore router AppStorage .saveSettings .clear() .set('role', ({role:PATIENT}) 'PATIENT') │ │ │ │ ▼ │ │ router.replaceUrl │ │ ({url:'pages/ │ │ HomePage'}) │ │ │ │ └───────────┼───────────┘ ▼ HomePage.aboutToAppear() │ ▼ 加载患者端数据 渲染患者端 UI
5.3 AppStorage.role 持久化
在 IVGuard 的实现中,角色信息同时存在于两个位置:
- DataStore(Preferences 持久化):
DataStore.loadSettings().role,应用重启后仍然存在 - AppStorage(内存状态):
AppStorage.get<string>('role'),应用运行期间可跨组件访问
两者的同步关系:
DataStore (Preferences) AppStorage (内存) │ │ │ aboutToAppear / 初始化 │ ├────────────────────────────→ │ │ settings.role │ AppStorage.set('role', value) │ │ │ 角色切换 │ │ DataStore.saveSettings() │ │ ──────────────────────────→ │ │ │ AppStorage.set('role', newValue) │ │ │ 应用重启 │ │ DataStore.loadSettings() │ │ ──────────────────────────→ │ │ │ AppStorage.set('role', savedRole)
AppStorage 的优势在于其与 @StorageLink / @StorageProp 装饰器的联动。任何使用 @StorageLink('role') 装饰的变量都会在角色切换时自动更新,无需手动监听:
` ypescript
@Component
struct RoleIndicator {
@StorageLink(‘role’) currentRole: string = ‘’
build() {
Text(当前角色: )
}
}
`
这种机制特别适用于需要在多个页面展示当前角色信息的场景,如顶部导航栏的角色标识、底部状态栏的角色提示等。当角色切换时,所有使用 @StorageLink('role') 的组件都会自动重新渲染,无需在每个页面的 aboutToAppear 中手动读取 DataStore。
5.4 返回 Index 重新选择
除了在 SettingsPage 中直接切换角色外,用户还可以选择"返回角色选择"功能,回到 Index 页面重新选择角色:
` ypescript
Button(‘返回角色选择’)
.onClick(() => {
const settings = DataStore.loadSettings()
settings.role = ‘’
DataStore.saveSettings(settings)
AppStorage.set(‘role’, ‘’)
router.clear()
router.replaceUrl({ url: 'pages/Index' })
})
`
这种设计适用于以下场景:
- 设备共享:同一台设备由不同角色的人轮流使用,每次使用前重新选择角色
- 角色确认:用户忘记当前角色,希望回到首页确认
- 演示场景:向不同人演示不同角色功能时,快速切换
- 初始配置:首次使用应用时,用户可能误选角色,需要重新选择
5.5 角色切换的数据隔离
角色切换不仅是导航状态的重置,还涉及数据源的切换。IVGuard 的各角色页面在 aboutToAppear 中加载数据时,会根据当前角色获取对应的数据:
` ypescript
aboutToAppear(): void {
const settings = DataStore.loadSettings()
this.currentRole = settings.role
this.loadData()
}
private loadData(): void {
if (this.currentRole === UserRole.PATIENT) {
this.loadPatientData()
} else if (this.currentRole === UserRole.NURSE) {
this.loadNurseData()
} else if (this.currentRole === UserRole.FAMILY) {
this.loadFamilyData()
}
}
`
对于共享页面(如 HospitalNavPage),需要根据当前角色差异化展示内容:
` ypescript
aboutToAppear(): void {
const settings = DataStore.loadSettings()
this.currentRole = settings.role
if (this.currentRole === UserRole.PATIENT) {
this.showMode = ‘navigation’
this.title = ‘院内导航’
} else if (this.currentRole === UserRole.FAMILY) {
this.showMode = ‘contact’
this.title = ‘联系护士’
}
}
`
角色切换时需清理的资源清单:
| 资源类型 | 清理方式 | 清理时机 |
|---|---|---|
| 定时器(setInterval) | clearInterval() | aboutToDisappear |
| 事件监听 | 取消订阅 | aboutToDisappear |
| 蓝牙连接 | 断开/保持 | 视业务需求 |
| 网络请求 | 取消未完成请求 | aboutToDisappear |
| @State 数据 | 置空/重置 | 角色切换时 |
| AppStorage 键值 | set(‘role’, newValue) | 角色切换时 |
角色切换的边界情况处理:
- 切换过程中的动画:角色切换涉及
router.clear()+router.replaceUrl()两步操作,中间可能存在短暂的空白画面。为优化体验,可以在切换前显示全屏 Loading 遮罩:
` ypescript
@State isSwitching: boolean = false
private switchRole(newRole: string): void {
this.isSwitching = true
setTimeout(() => {
const settings = DataStore.loadSettings()
settings.role = newRole
DataStore.saveSettings(settings)
router.clear()
router.replaceUrl({ url: this.getRoleHomePage(newRole) })
}, 300) // 等待 Loading 动画显示
}
`
-
切换过程中的网络请求:角色切换前,应取消当前页面未完成的网络请求,避免请求回调在新角色页面中执行导致数据混乱。可以在
aboutToDisappear中统一取消。 -
切换后的首次数据加载:新角色页面在
aboutToAppear中加载数据时,应显示 Loading 状态,避免空数据画面。
6. 导航状态管理
导航状态管理是 IVGuard 路由架构中需要持续关注的议题。不合理的导航状态管理会导致路由栈过深、重复跳转、数据刷新不及时等问题,直接影响用户体验。
6.1 返回栈控制
Router API 的返回栈(路由栈)是一个后进先出(LIFO)的数据结构,记录了用户的页面访问历史。理解并正确管理返回栈是构建流畅导航体验的基础。
6.1.1 pushUrl:入栈
pushUrl 将目标页面压入栈顶,当前页面保留在栈中。用户可通过 back() 返回上一页。
栈状态: [Index, HomePage]
执行: router.pushUrl({ url: 'pages/MedicinePage' })
栈状态: [Index, HomePage, MedicinePage]
这是最常用的路由操作,适用于大部分"前进"场景。
6.1.2 back():出栈
back() 将栈顶页面弹出,显示前一个页面。
栈状态: [Index, HomePage, MedicinePage]
执行: router.back()
栈状态: [Index, HomePage]
back() 支持指定返回目标:
router.back({ url: 'pages/HomePage' })
这会弹出栈顶页面直到找到指定的页面:
栈状态: [Index, HomePage, MedicinePage, MedicineDetailPage]
执行: router.back({ url: 'pages/HomePage' })
栈状态: [Index, HomePage]
在 IVGuard 中,back({ url: ... }) 常用于从深层页面快速返回 HomePage,避免用户多次点击返回键。
6.1.3 replaceUrl():替换栈顶
replaceUrl() 将当前栈顶页面替换为目标页面,不增加栈深度。
栈状态: [Index, HomePage]
执行: router.replaceUrl({ url: 'pages/NurseHomePage' })
栈状态: [Index, NurseHomePage]
适用于"不可返回"的场景,如:
- 角色选择后替换 Index 页面
- 登录成功后替换登录页
- 角色切换后替换旧角色的首页
6.1.4 clear():清空栈
clear() 清空路由栈中所有页面,仅保留当前页面。
栈状态: [Index, HomePage, MedicinePage, MedicineDetailPage]
执行: router.clear()
栈状态: [MedicineDetailPage]
注意:clear() 后当前页面仍在栈中。如需彻底重置,需配合 replaceUrl 使用。
6.1.5 各角色端的典型栈深度
| 角色端 | 最大栈深度 | 典型路径 |
|---|---|---|
| 患者端 | 5 | Index → HomePage → CostPage → CostDetailPage → InsuranceSetupPage |
| 护士端 | 3 | Index → NurseHomePage → PatientListPage |
| 家属端 | 3 | Index → FamilyHomePage → FamilyAlertPage |
患者端栈深度最大,主要原因是 Tab 切换使用 pushUrl 导致 HomePage 后面可能累积多个 Tab 页面。这是 MVP 阶段的已知问题,将在后续版本优化。
6.1.6 路由栈溢出风险
HarmonyOS 对路由栈的最大深度有限制(通常为 32 层)。虽然 IVGuard 的正常使用场景不会触及此限制,但如果用户反复切换 Tab,理论上可能栈溢出。Router API 在栈满时会拒绝 pushUrl 并抛出错误代码 100001。
预防措施:
private safePushUrl(url: string, params?: Object): void {
const stackLength = router.getLength()
if (stackLength >= 30) {
router.replaceUrl({ url: url, params: params })
} else {
router.pushUrl({ url: url, params: params })
}
}
6.2 防止重复 pushUrl
6.2.1 问题场景
在患者端 HomePage 的 Tab 切换中,每次切换 Tab 都会触发 router.pushUrl,导致路由栈持续增长。如果用户反复切换 Tab,栈深度会迅速膨胀:
操作 路由栈(从底到顶)
─────────────── ──────────────────────────────────────────────────────
初始状态 [Index, HomePage]
点击用药Tab [Index, HomePage, MedicinePage]
点击费用Tab [Index, HomePage, MedicinePage, CostPage]
点击用药Tab [Index, HomePage, MedicinePage, CostPage, MedicinePage]
点击费用Tab [Index, HomePage, MedicinePage, CostPage, MedicinePage, CostPage]
用户需要按 5 次返回键才能回到 HomePage,这显然不合理。
6.2.2 当前 MVP 方案
当前 MVP 版本中,Tab 切换直接使用 pushUrl,未做防抖处理:
onChange((index: number) => {
if (index === 1) {
router.pushUrl({ url: 'pages/MedicinePage' })
} else if (index === 2) {
router.pushUrl({ url: 'pages/HistoryPage' })
} else if (index === 3) {
router.pushUrl({ url: 'pages/AnalysisPage' })
} else if (index === 4) {
router.pushUrl({ url: 'pages/CostPage' })
}
})
6.2.3 优化方案一:Tab 内容内嵌
最根本的优化方案是将 Tab 内容以组件形式内嵌在 HomePage 中,而非跳转到独立页面:
Tabs() {
TabContent() {
MonitorTab({ onDetailClick: () => { ... }})
}
.tab('监护')
TabContent() {
MedicineTab()
}
.tab('用药')
TabContent() {
HistoryTab()
}
.tab('历史')
TabContent() {
AnalysisTab()
}
.tab('分析')
TabContent() {
CostTab()
}
.tab('费用')
}
这种方案的优点是完全消除了 Tab 切换的路由跳转,Tab 切换仅在组件层面进行,不影响路由栈。缺点是所有 Tab 内容都在 HomePage 中加载,增加了首页的初始化时间和内存占用。
6.2.4 优化方案二:replaceUrl 替代 pushUrl
如果仍需保持独立页面,可以使用 replaceUrl 替代 pushUrl 进行 Tab 切换:
onChange((index: number) => {
if (index === 1) {
router.replaceUrl({ url: 'pages/MedicinePage' })
} else if (index === 2) {
router.replaceUrl({ url: 'pages/HistoryPage' })
}
})
这样每次 Tab 切换都会替换当前页面而非入栈,路由栈始终保持浅层。缺点是用户无法通过返回键回到上一个 Tab,且丢失了 HomePage 的路由锚点。
6.2.5 优化方案三:防抖 + 路由守卫
更精细的方案是引入防抖和路由守卫机制:
@State lastTabSwitch: number = 0
onChange((index: number) => {
const now = Date.now()
if (now - this.lastTabSwitch < 500) {
return
}
this.lastTabSwitch = now
const targetUrl = this.getTabUrl(index)
if (targetUrl) {
router.pushUrl({ url: targetUrl })
}
})
private getTabUrl(index: number): string {
const tabMap: Record<number, string> = {
1: 'pages/MedicinePage',
2: 'pages/HistoryPage',
3: 'pages/AnalysisPage',
4: 'pages/CostPage'
}
return tabMap[index] || ''
}
6.2.6 各方案对比
| 方案 | 栈深度 | 代码改动量 | 用户体验 | 性能影响 |
|---|---|---|---|---|
| 当前(pushUrl) | 深 | 无 | 返回行为混乱 | 无 |
| 内嵌组件 | 最浅 | 大 | 流畅 | 首页加载稍慢 |
| replaceUrl | 浅 | 小 | 无法返回Tab | 无 |
| 防抖+守卫 | 中 | 中 | 较好 | 无 |
推荐方案:在下一个迭代中优先采用"Tab 内容内嵌"方案,彻底消除 Tab 切换的路由跳转。同时保留 MonitorPage 等需要独立页面的跳转。
6.3 页面生命周期与数据刷新
6.3.1 ArkUI 页面生命周期
ArkUI 的 @Entry 组件具有以下生命周期回调:
| 生命周期 | 触发时机 | 典型用途 |
|---|---|---|
aboutToAppear |
页面即将构建 UI 前 | 初始化数据、读取路由参数、发起网络请求 |
onPageShow |
页面每次显示时 | 刷新数据、恢复状态 |
onPageHide |
页面每次隐藏时 | 暂停动画、保存临时状态 |
aboutToDisappear |
页面即将销毁前 | 清理定时器、取消订阅、释放资源 |
onBackPress |
用户按返回键时 | 拦截返回行为、弹窗确认 |
6.3.2 数据刷新模式
IVGuard 的每个数据页面都实现了一个 loadData() 方法,在 aboutToAppear 和 onPageShow 中调用:
@Entry
@Component
struct MedicinePage {
@State medicineList: MedicineInfo[] = []
@State isLoading: boolean = false
aboutToAppear(): void {
this.loadData()
}
onPageShow(): void {
this.loadData()
}
private async loadData(): Promise<void> {
this.isLoading = true
try {
this.medicineList = await MedicineService.getList()
} catch (err) {
Logger.error('MedicinePage', '加载失败')
} finally {
this.isLoading = false
}
}
build() {
Column() {
if (this.isLoading) {
LoadingProgress().width(48).height(48)
} else {
List() {
ForEach(this.medicineList, (item: MedicineInfo) => {
ListItem() {
MedicineCard({ medicine: item })
}
})
}
}
}
}
}
aboutToAppear vs onPageShow 的选择:
aboutToAppear:仅在页面首次创建时调用,适合一次性初始化操作(如读取路由参数)onPageShow:每次页面显示时调用(包括从其他页面返回后),适合数据刷新
IVGuard 的策略是:在 aboutToAppear 中初始化参数和首次加载数据,在 onPageShow 中刷新数据。这确保了用户从详情页返回列表页时,列表数据会自动更新。
6.3.3 定时器清理
部分页面(如 MonitorPage)使用定时器实时刷新数据。定时器必须在 aboutToDisappear 中清理,否则会造成内存泄漏和无效的网络请求:
@Entry
@Component
struct MonitorPage {
@State monitorData: MonitorInfo | null = null
private refreshTimer: number = -1
aboutToAppear(): void {
this.loadData()
this.refreshTimer = setInterval(() => {
this.loadData()
}, 5000)
}
aboutToDisappear(): void {
if (this.refreshTimer !== -1) {
clearInterval(this.refreshTimer)
this.refreshTimer = -1
}
}
private async loadData(): Promise<void> {
this.monitorData = await MonitorService.getCurrentData()
}
build() {
// 监护页面 UI
}
}
定时器管理的最佳实践:
- 将定时器 ID 保存为组件的私有变量(非
@State,避免不必要的 UI 刷新) - 在
aboutToDisappear中统一清理 - 使用
-1或null作为"未设置"的标记值 - 清理后重置标记值,避免重复清理
6.3.4 页面返回时的数据刷新
当用户从详情页返回列表页时,列表页需要刷新数据以反映可能的变更。这通过 onPageShow 实现。
增量更新优化:
onPageShow(): void {
const lastUpdateTime = DataStore.getLastUpdateTime('medicine')
if (lastUpdateTime > this.lastLoadTime) {
this.loadData()
this.lastLoadTime = Date.now()
}
}
6.3.5 onBackPress 返回拦截
在部分场景下,需要拦截用户的返回行为。例如在监护页面中,如果正在持续输液,返回操作应弹出确认对话框:
onBackPress(): boolean {
if (this.isMonitoring) {
this.showExitConfirmDialog()
return true
}
return false
}
private showExitConfirmDialog(): void {
AlertDialog.show({
title: '确认退出',
message: '当前正在进行输液监护,确认退出监护页面?',
primaryButton: {
value: '取消',
action: () => {}
},
secondaryButton: {
value: '确认退出',
action: () => {
router.back()
}
}
})
}
onBackPress 的返回值语义:
- 返回
true:拦截返回,不执行默认返回操作 - 返回
false:不拦截,执行默认返回操作
这种机制在医疗应用中尤为重要——在关键操作(如输液监护、数据提交)进行中,不应允许用户随意退出。
6.3.6 各页面生命周期使用总结
| 页面 | aboutToAppear | onPageShow | aboutToDisappear | onBackPress |
|---|---|---|---|---|
| Index | 读取角色、自动跳转 | - | - | - |
| HomePage | 初始化Tab | 刷新监护数据 | - | - |
| MonitorPage | 加载监护数据、启动定时器 | 刷新数据 | 清理定时器 | 拦截返回 |
| MedicinePage | 加载用药列表 | 刷新列表 | - | - |
| MedicineDetailPage | 读取medicineId、加载详情 | - | - | - |
| CostPage | 加载费用列表 | 刷新列表 | - | - |
| CostDetailPage | 读取costItemId、加载详情 | - | - | - |
| SettingsPage | 读取当前设置 | - | - | - |
| HospitalNavPage | 根据角色加载 | - | - | - |
| NurseHomePage | 加载护士数据 | 刷新 | - | - |
| FamilyHomePage | 读取患者信息 | 刷新 | - | - |
7. 跨角色共享页面
IVGuard 的三个角色端(患者、护士、家属)虽然拥有各自独立的路由子树,但部分页面在多角色间共享。共享页面通过角色检测机制动态调整显示内容,避免为每个角色创建独立的页面文件,减少代码重复和维护成本。
7.1 共享页面清单
| 共享页面 | 使用角色 | 功能差异 |
|---|---|---|
| SettingsPage | 患者、护士、家属 | 角色切换目标不同,数据源不同 |
| HospitalNavPage | 患者、家属 | 患者:院内导航;家属:联系护士 |
7.2 SettingsPage 共享设计
SettingsPage 是 IVGuard 中唯一一个三角色完全共享的页面。不同角色用户看到的设置选项基本相同,但"切换角色"的目标页面不同。
7.2.1 角色感知的实现
SettingsPage 通过 DataStore.loadSettings().role 获取当前角色信息,据此调整 UI 和行为:
@Entry
@Component
struct SettingsPage {
@State currentRole: string = ''
@State patientId: string = ''
@State nurseId: string = ''
aboutToAppear(): void {
const settings = DataStore.loadSettings()
this.currentRole = settings.role || ''
this.patientId = settings.patientId || ''
this.nurseId = settings.nurseId || ''
}
build() {
Scroll() {
Column() {
// 当前角色标识
Row() {
Text('当前角色:')
.fontSize(16)
Text(this.getRoleDisplayName(this.currentRole))
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#007DFF')
}
.margin({ bottom: 20 })
// 通用设置项
this.BuildFontSizeSetting()
this.BuildThemeSetting()
this.BuildNotificationSetting()
Divider().margin({ top: 20, bottom: 20 })
// 角色切换
Text('切换角色')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 15 })
Button('切换为患者')
.width('80%')
.height(50)
.onClick(() => this.switchRole(UserRole.PATIENT))
Button('切换为护士')
.width('80%')
.height(50)
.margin({ top: 15 })
.onClick(() => this.switchRole(UserRole.NURSE))
Button('切换为家属')
.width('80%')
.height(50)
.margin({ top: 15 })
.onClick(() => this.switchRole(UserRole.FAMILY))
Button('返回角色选择')
.width('80%')
.height(50)
.margin({ top: 30 })
.backgroundColor('#FF4444')
.onClick(() => {
const settings = DataStore.loadSettings()
settings.role = ''
DataStore.saveSettings(settings)
router.clear()
router.replaceUrl({ url: 'pages/Index' })
})
// 角色特有设置
if (this.currentRole === UserRole.PATIENT) {
this.BuildPatientSettings()
} else if (this.currentRole === UserRole.NURSE) {
this.BuildNurseSettings()
} else if (this.currentRole === UserRole.FAMILY) {
this.BuildFamilySettings()
}
}
.padding(20)
}
}
@Builder
BuildPatientSettings(): void {
Divider().margin({ top: 20, bottom: 20 })
Text('患者专属设置').fontSize(18).fontWeight(FontWeight.Bold)
// 患者特有的设置项,如输液提醒阈值、告警声音等
}
@Builder
BuildNurseSettings(): void {
Divider().margin({ top: 20, bottom: 20 })
Text('护士专属设置').fontSize(18).fontWeight(FontWeight.Bold)
// 护士特有的设置项,如刷新频率、管辖床位范围等
}
@Builder
BuildFamilySettings(): void {
Divider().margin({ top: 20, bottom: 20 })
Text('家属专属设置').fontSize(18).fontWeight(FontWeight.Bold)
// 家属特有的设置项,如告警推送开关、关注患者列表等
}
private getRoleDisplayName(role: string): string {
if (role === UserRole.PATIENT) return '患者'
if (role === UserRole.NURSE) return '护士'
if (role === UserRole.FAMILY) return '家属'
return '未选择'
}
private switchRole(newRole: string): void {
const settings = DataStore.loadSettings()
settings.role = newRole
DataStore.saveSettings(settings)
AppStorage.set<string>('role', newRole)
router.clear()
router.replaceUrl({ url: this.getRoleHomePage(newRole) })
}
private getRoleHomePage(role: string): string {
if (role === UserRole.PATIENT) return 'pages/HomePage'
if (role === UserRole.NURSE) return 'pages/NurseHomePage'
if (role === UserRole.FAMILY) return 'pages/FamilyHomePage'
return 'pages/Index'
}
}
7.3 HospitalNavPage 共享设计
HospitalNavPage 在患者端和家属端之间共享,但两个角色看到的内容和功能完全不同:
- 患者端:显示院内导航地图,帮助患者找到输液室、药房、缴费处等位置
- 家属端:显示联系护士功能,包括护士站位置、一键呼叫、留言等
7.3.1 角色差异化实现
@Entry
@Component
struct HospitalNavPage {
@State currentRole: string = ''
@State showMode: string = 'navigation'
@State title: string = ''
aboutToAppear(): void {
const settings = DataStore.loadSettings()
this.currentRole = settings.role
if (this.currentRole === UserRole.PATIENT) {
this.showMode = 'navigation'
this.title = '院内导航'
} else if (this.currentRole === UserRole.FAMILY) {
this.showMode = 'contact'
this.title = '联系护士'
}
}
build() {
Column() {
// 标题栏
Row() {
Image($r('app.media.ic_back'))
.width(24)
.height(24)
.onClick(() => router.back())
Text(this.title)
.fontSize(20)
.fontWeight(FontWeight.Bold)
.layoutWeight(1)
.textAlign(TextAlign.Center)
Blank().width(24)
}
.width('100%')
.height(56)
.padding({ left: 16, right: 16 })
if (this.showMode === 'navigation') {
this.BuildNavigationView()
} else if (this.showMode === 'contact') {
this.BuildContactView()
}
}
.width('100%')
.height('100%')
}
@Builder
BuildNavigationView(): void {
Column() {
// 院内导航地图和位置列表
Text('院内导航地图')
.fontSize(16)
.margin({ top: 20 })
List() {
ListItem() {
NavItem({ name: '输液室', floor: '3F', distance: '50m' })
}
ListItem() {
NavItem({ name: '药房', floor: '1F', distance: '120m' })
}
ListItem() {
NavItem({ name: '缴费处', floor: '1F', distance: '150m' })
}
}
}
}
@Builder
BuildContactView(): void {
Column() {
// 联系护士功能
Text('护士站信息')
.fontSize(16)
.margin({ top: 20 })
Text('护士站: A区3楼护士站')
.fontSize(14)
.margin({ top: 10 })
Button('一键呼叫护士')
.width('80%')
.height(50)
.margin({ top: 20 })
.backgroundColor('#007DFF')
.onClick(() => {
this.callNurse()
})
Button('给护士留言')
.width('80%')
.height(50)
.margin({ top: 15 })
.onClick(() => {
this.leaveMessage()
})
}
}
private callNurse(): void {
// 实现呼叫护士逻辑
}
private leaveMessage(): void {
// 实现留言逻辑
}
}
7.4 共享页面的设计原则
- 角色检测前置:在
aboutToAppear中尽早获取角色信息,避免 UI 闪烁 - UI 分离:不同角色的 UI 逻辑封装在独立的
@Builder方法中,不混在一起 - 参数差异化:共享页面不通过路由参数区分角色(因为角色信息已持久化),而是从 DataStore 读取
- 可扩展性:新增角色时,只需在
aboutToAppear中增加角色判断分支,添加对应的@Builder方法 - 避免过度共享:只有当两个或更多角色的页面功能高度相似时才共享。如果页面内容差异超过 50%,应考虑拆分为独立页面
7.5 共享页面 vs 独立页面的决策准则
| 判断条件 | 共享页面 | 独立页面 |
|---|---|---|
| 页面结构相似度 | > 70% | < 70% |
| 业务逻辑差异 | 仅数据源不同 | 核心逻辑不同 |
| 参数传递方式 | 通过 DataStore | 通过路由参数 |
| 未来扩展性 | 不太可能分化 | 可能各自演化 |
| 维护成本 | 一处修改 | 多处同步修改 |
以 HospitalNavPage 为例,虽然患者端和家属端的显示内容不同,但它们的页面结构(标题栏+内容区+返回按钮)高度相似,且共享同一套导航框架,因此选择共享。
8. 深度链接与跨应用导航(未来)
深度链接(Deep Linking)是 IVGuard 未来版本的重要扩展方向。通过深度链接,外部应用可以直接跳转到 IVGuard 的指定页面,实现跨应用的无缝导航。这在医院生态系统中具有重要价值——医生使用 HIS 系统查看患者信息时,可以直接跳转到 IVGuard 的监护页面。
8.1 scheme 配置
IVGuard 计划使用 ivguard:// 作为自定义 URI scheme,在应用的 module.json5 中配置:
{
"module": {
"abilities": [
{
"name": "EntryAbility",
"skills": [
{
"actions": ["action.system.home"],
"entities": ["entity.system.home"],
"uris": [
{
"scheme": "ivguard",
"host": "app",
"pathStartWith": "/"
}
]
}
]
}
]
}
}
8.2 URL 路由映射
深度链接的 URL 路径映射到 IVGuard 的页面路由:
ivguard://app/monitor?sessionId=session_abc → MonitorPage(sessionId='session_abc')
ivguard://app/medicine/detail?id=med_001 → MedicineDetailPage(medicineId='med_001')
ivguard://app/cost/detail?id=cost_2024_001 → CostDetailPage(costItemId='cost_2024_001')
ivguard://app/patient/list?nurse=A301 → PatientListPage(bedNumber='A301')
ivguard://app/family/alert?patientId=p001 → FamilyAlertPage(patientId='p001')
ivguard://app/settings → SettingsPage()
8.3 路由解析实现
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit'
import { window } from '@kit.ArkUI'
export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
if (want.uri) {
this.handleDeepLink(want.uri)
}
}
onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
if (want.uri) {
this.handleDeepLink(want.uri)
}
}
private handleDeepLink(uri: string): void {
const url = new URL(uri)
const path = url.pathname
const params = url.searchParams
if (path === '/monitor') {
AppStorage.set<string>('deepLinkPage', 'pages/MonitorPage')
AppStorage.set<string>('deepLinkSessionId', params.get('sessionId') || '')
} else if (path === '/medicine/detail') {
AppStorage.set<string>('deepLinkPage', 'pages/MedicineDetailPage')
AppStorage.set<string>('deepLinkMedicineId', params.get('id') || '')
} else if (path === '/cost/detail') {
AppStorage.set<string>('deepLinkPage', 'pages/CostDetailPage')
AppStorage.set<string>('deepLinkCostItemId', params.get('id') || '')
}
}
}
8.4 跨应用跳转场景
-
从医院 HIS 系统跳转:医生在 HIS 系统中点击"查看监护"按钮,跳转到 IVGuard 的 MonitorPage,直接查看指定患者的实时监护数据
-
从护士工作站跳转:护士在工作站中点击"告警详情",跳转到 IVGuard 的 FamilyAlertPage(以家属视角查看告警)
-
从推送通知跳转:用户点击系统推送通知(如"输液即将完成"),直接跳转到 MonitorPage
-
从医院导诊 App 跳转:患者在导诊 App 中点击"前往输液室",跳转到 IVGuard 的 HospitalNavPage
8.5 深度链接的安全考虑
-
角色验证:深度链接跳转前需验证当前用户角色是否有权访问目标页面。例如,只有护士角色才能通过深度链接访问 PatientListPage
-
参数校验:对深度链接传入的参数进行严格校验,防止注入攻击
-
来源白名单:配置允许发起深度链接的应用白名单,拒绝未知来源的跳转请求
-
数据脱敏:深度链接中不应包含敏感信息(如患者身份证号),只传递业务 ID
8.6 深度链接的实现时间线
| 版本 | 功能 | 优先级 |
|---|---|---|
| v1.1 | scheme 注册 + 基础路由解析 | P1 |
| v1.2 | 推送通知跳转 | P1 |
| v1.3 | 跨应用跳转 + 来源白名单 | P2 |
| v2.0 | 通用链接(Universal Link)+ HTTPS scheme | P3 |
附录 A:完整路由参数接口清单
以下是 RouteParams.ets 中定义的所有路由参数接口,以及对应的页面和使用场景:
| 接口名 | 字段 | 对应页面 | 传递方 | 使用场景 |
|---|---|---|---|---|
| MonitorParams | sessionId: string | MonitorPage | HomePage / MonitorTab | 查看指定会话的监护详情 |
| MedicineDetailParams | medicineId: string | MedicineDetailPage | MedicinePage | 查看指定用药的详细信息 |
| CostDetailParams | costItemId: string | CostDetailPage | CostPage | 查看指定费用项的详情 |
| NursePatientParams | patientName: string, bedNumber: string | PatientListPage | NurseHomePage | 查看指定患者的信息 |
| FamilyPatientParams | patientId: string, patientName: string | FamilyAlertPage | FamilyHomePage | 查看指定患者的告警 |
未来可能新增的接口:
export interface InsuranceSetupParams {
insuranceId: string
}
export interface HistoryDetailParams {
recordId: string
}
export interface NursePatientDetailParams {
patientId: string
fromList: boolean
}
export interface AlertDetailParams {
alertId: string
alertLevel: string
}
附录 B:路由常见问题 FAQ
Q1:页面跳转后白屏,如何排查?
A:按照以下顺序检查:
- 页面文件是否存在于
pages/目录 - 页面是否在
main_pages.json中注册 pushUrl中的路径与注册路径是否完全一致(含大小写)- 页面的
aboutToAppear是否有未捕获异常 - 页面的
build方法是否正确返回了 UI 组件
详见第 4.2 节的白屏排查决策树。
Q2:路由参数传递后为空,可能的原因?
A:常见原因包括:
- 参数对象中包含不可序列化的数据(函数、Date 实例等)
- 在
build方法而非aboutToAppear中调用getParams() - 使用了
replaceUrl但新页面未重新获取参数 - 参数字段名拼写错误(TypeScript 的
as断言不会校验字段名)
Q3:如何实现"返回到指定页面"?
A:使用 router.back() 的 URL 参数:
router.back({ url: 'pages/HomePage' })
这会弹出栈顶页面直到找到 HomePage。注意:如果栈中不存在指定页面,back() 会弹出所有页面直到栈底。
Q4:Tab 切换导致路由栈过深,如何优化?
A:推荐方案是将 Tab 内容以组件形式内嵌在 HomePage 中,避免 pushUrl 跳转。详见第 6.2.3 节。
Q5:角色切换后返回键回到旧角色页面,如何解决?
A:使用 router.clear() + router.replaceUrl() 的组合,彻底重置路由栈。详见第 5.2 节。
Q6:如何在页面间传递大量数据?
A:路由参数只适合传递小型标识数据(如 ID)。大量数据应通过 DataStore(Preferences 持久化)或 AppStorage(内存状态)传递,目标页面从全局状态中读取。
Q7:Navigation 方案什么时候应该迁移?
A:当出现以下需求时考虑迁移:
- 需要自定义转场动画
- 需要路由拦截(如权限校验)
- 需要分栏模式适配平板设备
- 页面数量超过 30 个,需要更系统化的路由管理
Q8:如何防止用户快速双击导致重复跳转?
A:引入防抖机制,在跳转后短时间内禁止重复跳转:
private lastNavigateTime: number = 0
private navigateWithDebounce(url: string, params?: Object): void {
const now = Date.now()
if (now - this.lastNavigateTime < 800) {
return
}
this.lastNavigateTime = now
router.pushUrl({ url: url, params: params })
}
Q9:router.getLength() 返回值不准确?
A:router.getLength() 返回的是路由栈中的页面数量,包括所有通过 pushUrl 入栈的页面。如果页面使用了 replaceUrl 或 clear(),栈深度会相应减少。注意 getLength() 是同步方法,调用时获取的是当前时刻的栈状态。
Q10:共享页面如何避免角色判断遗漏?
A:在共享页面的 aboutToAppear 中,使用 if-else 链覆盖所有已知角色,并在最后添加默认处理逻辑(如跳转回 Index)。同时,在代码审查时重点检查共享页面的角色分支是否完整。
附录 C:版本演进规划
v1.0(当前)—— Router API + 基础多角色分发
- 采用 Router API 作为路由方案
- Index 页面角色选择 + DataStore 持久化
- 三个角色端独立的路由子树
- SettingsPage 角色切换
- 基础的路由参数传递
v1.1 —— 路由优化与稳定性提升
- Tab 内容内嵌化,消除 Tab 切换的
pushUrl - 路由防抖机制
- PageRoutes 路径常量化
- 路由错误处理(跳转失败提示)
safePushUrl栈溢出保护
v1.2 —— 深度链接与跨应用导航
ivguard://scheme 注册- URL 路由映射
- 推送通知跳转
- 参数校验与安全加固
v2.0 —— Navigation 方案评估与迁移
- 评估 Navigation + NavPathStack 方案
- 自定义转场动画
- 路由拦截鉴权
- 分栏模式适配平板
- 路由栈可视化调试工具
v3.0 —— 企业级路由框架
- 声明式路由配置(类似 Vue Router 的路由表)
- 路由守卫中间件
- 路由懒加载
- 路由级代码分割
- 服务端路由(SSR)预研
更多推荐


所有评论(0)