基于鸿蒙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 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-角色选择页

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

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

  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 可能被多次调用

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

页面 参数接口 接收时机 数据加载方式
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" ]

注册规则

  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 各角色端的典型栈深度
角色端 最大栈深度 典型路径
患者端 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() 方法,在 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 各页面生命周期使用总结
页面 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 共享页面的设计原则

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

  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开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐