基于鸿蒙OS开发静脉输液智能监控系统(20)-多角色路由与导航框架


目录


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() 读取。这种模式的关键约束如下:

  1. 参数序列化params 对象在传递过程中会经历序列化/反序列化,因此只支持可序列化的数据类型(基本类型、数组、普通对象)。函数、class 实例方法、@State 装饰的代理对象等不可序列化数据无法正确传递。

  2. 参数时效性router.getParams() 返回的参数对象在页面存活期间一直可用,但当页面被 replaceUrl 替换后,新页面需要重新接收参数。

  3. 参数类型安全:由于 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 路由回调处理

pushUrlreplaceUrl 均为异步操作,支持 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 作为路由方案,基于以下考量:

  1. 学习曲线低:Router API 的函数式调用风格对所有团队成员来说都易于理解和上手,无需额外学习 Navigation 容器的声明式路由概念。在 MVP 阶段,快速迭代比架构完美更重要。

  2. 代码简洁:每次路由跳转仅需一行函数调用,无需在 Navigation 容器中预注册 NavDestination,代码量显著减少。

  3. 与多角色分发契合:IVGuard 的 Index 页面根据角色选择分发到不同的 HomePage,这种"一次性分发"模式天然适合 pushUrl,无需复杂的路由拦截逻辑。

  4. 调试方便:Router API 的路由栈行为直观可预测,开发者可通过 router.getLength() 获取栈深度,快速定位导航问题。

  5. 生态成熟: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 的路由栈管理具有更细粒度的控制能力:

  • 路由拦截:可通过 onWillAppearonWillShow 等回调实现路由拦截,例如权限校验、数据预加载
  • 动画自定义:支持自定义页面转场动画,包括入场、出场、共享元素转场
  • 路由守卫:可在路由跳转前/后执行逻辑,类似 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 优势与劣势分析

优势

  1. 动画自定义:Navigation 支持丰富的转场动画配置,包括 NavTransition 动画对象,可实现 iOS 风格的滑入滑出、Android 风格的淡入淡出、以及自定义共享元素转场动画。对于医疗监护应用,流畅的转场动画能提升用户信任感。

  2. 路由拦截:通过 NavigationonNavBarStateChangeonNavigationModeChange 以及 NavPathStack 的事件监听,可以在路由跳转前后执行拦截逻辑。例如在跳转到 MonitorPage 前检查蓝牙连接状态,未连接则拦截并引导用户先配对设备。

  3. 声明式范式:与 ArkUI 的声明式 UI 范式一致,路由配置和 UI 布局写在同一处,代码内聚性更强。

  4. Toolbar/NavBar 自定义:Navigation 容器内置了标题栏、工具栏的配置能力,无需手动实现。

  5. 路由模式切换:支持 NavigationMode.Stack(栈模式)和 NavigationMode.Split(分栏模式),适配不同屏幕尺寸。

劣势

  1. 学习曲线较陡:开发者需要理解 Navigation 容器、NavDestination、NavPathStack 三者的关系和协作方式,概念较多。对于团队新成员或外包协作来说,上手成本高于 Router API。

  2. 页面注册复杂:每个页面都需要以 NavDestination 形式注册,且需要在 navDestination Builder 中手动编写条件分发逻辑。当页面数量增多(IVGuard 已有 17+ 个页面)时,维护成本显著上升。

  3. 嵌套层级深:Navigation 容器包裹所有页面,增加了组件嵌套层级,在某些场景下可能影响布局调试。

  4. 与 Tabs 组件配合的复杂性:IVGuard 患者端 HomePage 采用 5-Tab 布局,Tabs 组件嵌入 Navigation 容器内的实现方式较为复杂,需要处理 Tab 切换与路由栈的交互。

  5. MVP 阶段的过度设计风险:Navigation 方案的诸多高级特性(动画自定义、路由拦截、分栏模式)在 MVP 阶段并非必需,投入学习成本但短期内无法兑现收益。

1.3 两种方案对比

下表从多个维度对 Router API 和 Navigation + NavPathStack 进行系统对比:

特性Router APINavigation + NavPathStack
使用方式函数调用(router.pushUrl()声明式容器 + 路由栈操作
页面注册main_pages.json 静态配置navDestination Builder 动态注册
转场动画系统默认,不可自定义完全可自定义(NavTransition
路由拦截无原生支持支持(onWillShow / onWillAppear
参数传递params 对象(序列化)对象引用(可传递不可序列化数据)
路由栈控制pushUrl / back / replaceUrl / clearpushPath / 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-角色选择页

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

患者列表页展示护士负责的所有患者,跳转时携带 patientNamebedNumber 参数:

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

告警通知页展示家属关注的患者的告警历史,跳转时携带 patientIdpatientName 以便加载对应患者的告警数据:

` 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)NurseHomePageFamilyHomePage
页面数量1033
最大栈深度533
有参数页面311
共享页面SettingsPage, HospitalNavPageSettingsPageSettingsPage, HospitalNavPage
Tab导航5个Tab
子页面最深路径CostPage -> CostDetailPagePatientListPageFamilyAlertPage

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
}
`

每个接口的设计遵循以下原则:

  1. 接口命名规范{PageName}Params,其中 PageName 为目标页面的名称去掉 “Page” 后缀。例如 MedicineDetailParams 对应 MedicineDetailPage

  2. 字段最小化:每个接口只包含目标页面必需的参数,不传递冗余数据。例如 MedicineDetailParams 只传 medicineId,而非整个 MedicineInfo 对象,详情页自行根据 ID 查询完整数据。

  3. 基本类型优先:参数字段只使用 stringnumberboolean 等基本类型,避免传递复杂对象。这既符合 Router API 的序列化约束,也降低了页面间的数据耦合度。

  4. 只传 ID,不传对象:这是 IVGuard 路由参数设计的核心原则。传递 ID 而非完整对象有以下优势:

    • 避免序列化/反序列化导致的数据丢失(如 Date 对象会变为字符串)
    • 目标页面总是获取最新数据,而非跳转时的快照
    • 参数接口稳定,不随业务对象字段变更而频繁修改

设计权衡:传 ID 还是传对象?

方案优势劣势适用场景
只传 ID参数稳定、数据最新、解耦目标页需额外请求数据数据可能变化的场景
传完整对象目标页无需请求、响应快参数易变、耦合度高数据不变的静态场景
混合方案灵活复杂度高复杂业务场景

IVGuard 选择"只传 ID"方案,原因如下:

  • 医疗数据具有时效性,患者费用、用药信息随时可能更新,目标页面应获取最新数据
  • 网络延迟在医疗场景下可接受(通常 < 500ms),且可在目标页面展示 Loading 状态
  • 保持参数接口的稳定性,避免因业务对象字段增删而频繁修改路由参数

3.2 参数传递

参数传递统一通过 router.pushUrlparams 字段实现,并使用 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' })

参数传递的注意事项

  1. 参数必须可序列化:Router API 会对 params 对象进行序列化处理,因此参数值只能是基本类型、数组、普通对象。以下类型不可传递

    • 函数(Function
    • class 实例(包含方法的类实例)
    • @State / @Prop 装饰的代理对象
    • Date 对象(会变为字符串)
    • undefined(会变为 null
  2. 参数大小限制:虽然 HarmonyOS 未明确限制参数大小,但考虑到序列化性能和内存占用,建议单个参数对象不超过 1KB。对于大数据传递,应使用 DataStore 等全局状态管理方案。

  3. 参数时机params 只能在 pushUrl / replaceUrl 时传递,back() 不支持携带参数。如果需要在返回时传递数据,应使用 AppStorage 或 DataStore 等全局状态机制。

  4. 类型断言的安全性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 中一次性获取并保存到组件状态中,避免在后续逻辑中重复调用。这是因为:

  1. aboutToAppear 在页面构建前调用,参数获取后可立即用于数据加载
  2. 将参数保存到 @State 变量后,UI 可自动响应数据变化
  3. 避免在 build 方法中调用 getParams(),因为 build 可能被多次调用

各角色端的参数接收模式总结

页面参数接口接收时机数据加载方式
MonitorPageMonitorParamsaboutToAppear根据 sessionId 查询监护数据
MedicineDetailPageMedicineDetailParamsaboutToAppear根据 medicineId 查询用药详情
CostDetailPageCostDetailParamsaboutToAppear根据 costItemId 查询费用详情
PatientListPageNursePatientParamsaboutToAppear根据 patientName+bedNumber 筛选
FamilyAlertPageFamilyPatientParamsaboutToAppear根据 patientId 查询告警列表
MedicinePageaboutToAppear加载当前患者的全部用药
HistoryPageaboutToAppear加载当前患者的历史记录
AnalysisPageaboutToAppear加载当前患者的分析数据
CostPageaboutToAppear加载当前患者的费用列表
SettingsPageaboutToAppear加载当前设置
HospitalNavPageaboutToAppear根据角色加载导航/联系信息

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" ]

注册规则

  1. 路径格式:页面路径以 pages/ 开头,不含文件扩展名(.ets 省略)。
  2. 首项必须为 Index:数组的第一个元素必须是应用的入口页面(通常为 pages/Index)。
  3. 顺序无关:除首项外,其余页面的注册顺序不影响路由功能,但建议按功能模块分组排列以便维护。
  4. 唯一性:每个页面路径只能出现一次,重复注册会导致编译警告。

按角色分组的推荐排列方式

`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 找不到注册信息,无法正确加载页面。

排查步骤

`

  1. 确认页面文件路径
    → 检查 pages/SomePage.ets 是否存在

  2. 检查 main_pages.json
    → 搜索 “pages/SomePage” 是否在注册列表中

  3. 对比路径大小写
    → “pages/SomePage” vs “pages/somepage” 是否一致

  4. 添加注册并重新编译
    → 在 main_pages.json 中添加 “pages/SomePage”
    `

4.2.2 场景二:页面注册但文件不存在

现象:编译阶段报错,提示找不到页面文件。

原因main_pages.json 中注册了某个页面路径,但对应的 .ets 文件不存在。

错误信息示例

Error: Cannot find module 'pages/NonExistentPage'

排查步骤

`

  1. 检查编译错误信息
    → 确认哪个页面路径报错

  2. 确认页面文件是否遗漏
    → 检查 pages/ 目录下是否存在对应文件

  3. 如果文件确实不需要,从 main_pages.json 中移除注册
    → 删除对应的注册条目
    `

4.2.3 场景三:路径大小写不一致

现象:调用 router.pushUrl({ url: 'pages/monitorPage' }) 后白屏。

原因:页面文件名为 MonitorPage.ets,注册路径为 pages/MonitorPage,但跳转时写成了 pages/monitorPage(小写 m)。HarmonyOS 的页面路径是大小写敏感的,路径不匹配会导致白屏。

排查步骤

`

  1. 对比三处路径
    → 文件名: MonitorPage.ets
    → main_pages.json: “pages/MonitorPage”
    → pushUrl 调用: ‘pages/monitorPage’ ← 这里不一致!

  2. 统一路径命名
    → 确保所有页面路径使用 PascalCase 命名

  3. 建立路径常量
    → 在 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 建议在开发流程中引入以下措施:

  1. 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(注册但文件不存在的页面: )
}
`

  1. Code Review 检查清单:在代码审查时,凡是涉及新增/删除页面的 PR,必须确认 main_pages.json 同步更新。

  2. 路径常量强制使用:团队约定所有路由跳转必须使用 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 的实现中,角色信息同时存在于两个位置:

  1. DataStore(Preferences 持久化)DataStore.loadSettings().role,应用重启后仍然存在
  2. 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' })

})
`

这种设计适用于以下场景:

  1. 设备共享:同一台设备由不同角色的人轮流使用,每次使用前重新选择角色
  2. 角色确认:用户忘记当前角色,希望回到首页确认
  3. 演示场景:向不同人演示不同角色功能时,快速切换
  4. 初始配置:首次使用应用时,用户可能误选角色,需要重新选择

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)角色切换时

角色切换的边界情况处理

  1. 切换过程中的动画:角色切换涉及 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 动画显示
}
`

  1. 切换过程中的网络请求:角色切换前,应取消当前页面未完成的网络请求,避免请求回调在新角色页面中执行导致数据混乱。可以在 aboutToDisappear 中统一取消。

  2. 切换后的首次数据加载:新角色页面在 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 各角色端的典型栈深度
角色端最大栈深度典型路径
患者端5Index → HomePage → CostPage → CostDetailPage → InsuranceSetupPage
护士端3Index → NurseHomePage → PatientListPage
家属端3Index → 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() 方法,在 aboutToAppearonPageShow 中调用:

@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
  }
}

定时器管理的最佳实践

  1. 将定时器 ID 保存为组件的私有变量(非 @State,避免不必要的 UI 刷新)
  2. aboutToDisappear 中统一清理
  3. 使用 -1null 作为"未设置"的标记值
  4. 清理后重置标记值,避免重复清理
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 各页面生命周期使用总结
页面aboutToAppearonPageShowaboutToDisappearonBackPress
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 共享页面的设计原则

  1. 角色检测前置:在 aboutToAppear 中尽早获取角色信息,避免 UI 闪烁
  2. UI 分离:不同角色的 UI 逻辑封装在独立的 @Builder 方法中,不混在一起
  3. 参数差异化:共享页面不通过路由参数区分角色(因为角色信息已持久化),而是从 DataStore 读取
  4. 可扩展性:新增角色时,只需在 aboutToAppear 中增加角色判断分支,添加对应的 @Builder 方法
  5. 避免过度共享:只有当两个或更多角色的页面功能高度相似时才共享。如果页面内容差异超过 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 跨应用跳转场景

  1. 从医院 HIS 系统跳转:医生在 HIS 系统中点击"查看监护"按钮,跳转到 IVGuard 的 MonitorPage,直接查看指定患者的实时监护数据

  2. 从护士工作站跳转:护士在工作站中点击"告警详情",跳转到 IVGuard 的 FamilyAlertPage(以家属视角查看告警)

  3. 从推送通知跳转:用户点击系统推送通知(如"输液即将完成"),直接跳转到 MonitorPage

  4. 从医院导诊 App 跳转:患者在导诊 App 中点击"前往输液室",跳转到 IVGuard 的 HospitalNavPage

8.5 深度链接的安全考虑

  1. 角色验证:深度链接跳转前需验证当前用户角色是否有权访问目标页面。例如,只有护士角色才能通过深度链接访问 PatientListPage

  2. 参数校验:对深度链接传入的参数进行严格校验,防止注入攻击

  3. 来源白名单:配置允许发起深度链接的应用白名单,拒绝未知来源的跳转请求

  4. 数据脱敏:深度链接中不应包含敏感信息(如患者身份证号),只传递业务 ID

8.6 深度链接的实现时间线

版本功能优先级
v1.1scheme 注册 + 基础路由解析P1
v1.2推送通知跳转P1
v1.3跨应用跳转 + 来源白名单P2
v2.0通用链接(Universal Link)+ HTTPS schemeP3

附录 A:完整路由参数接口清单

以下是 RouteParams.ets 中定义的所有路由参数接口,以及对应的页面和使用场景:

接口名字段对应页面传递方使用场景
MonitorParamssessionId: stringMonitorPageHomePage / MonitorTab查看指定会话的监护详情
MedicineDetailParamsmedicineId: stringMedicineDetailPageMedicinePage查看指定用药的详细信息
CostDetailParamscostItemId: stringCostDetailPageCostPage查看指定费用项的详情
NursePatientParamspatientName: string, bedNumber: stringPatientListPageNurseHomePage查看指定患者的信息
FamilyPatientParamspatientId: string, patientName: stringFamilyAlertPageFamilyHomePage查看指定患者的告警

未来可能新增的接口

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:按照以下顺序检查:

  1. 页面文件是否存在于 pages/ 目录
  2. 页面是否在 main_pages.json 中注册
  3. pushUrl 中的路径与注册路径是否完全一致(含大小写)
  4. 页面的 aboutToAppear 是否有未捕获异常
  5. 页面的 build 方法是否正确返回了 UI 组件

详见第 4.2 节的白屏排查决策树。

Q2:路由参数传递后为空,可能的原因?

A:常见原因包括:

  1. 参数对象中包含不可序列化的数据(函数、Date 实例等)
  2. build 方法而非 aboutToAppear 中调用 getParams()
  3. 使用了 replaceUrl 但新页面未重新获取参数
  4. 参数字段名拼写错误(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() 返回值不准确?

Arouter.getLength() 返回的是路由栈中的页面数量,包括所有通过 pushUrl 入栈的页面。如果页面使用了 replaceUrlclear(),栈深度会相应减少。注意 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)预研
Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐