【HarmonyOS 7 平行视界深度实战】04 导航模式怎么保持左栏稳定、右栏连续切换
前言
用户点击草稿箱以后,右侧已经显示草稿,左侧却仍然高亮收件箱。邮箱放进宽窗口时,这种错位会同时出现在眼前:分类栏虽然一直存在,却给出了错误的位置提示。设置目录和知识库章节也会遇到同样的问题,入口需要保留,选中项还要跟着当前内容更新。
平行视界的导航模式可以把主页区域留在左侧,让右侧显示当前路由栈顶页。系统确定两栏位置以后,应用仍要维护分类选择和邮件数据。要解决开头的错位,需要先让主页与默认内容建立正确关系,再让一次分类点击同时改变左栏高亮和右栏路由。

一、冷启动应当显示哪个分类
分类入口要持续位于左侧,EasyGo 就需要知道哪一页是主页。当前工程把 navigationSplitOptions.mode 设为 1,使用导航模式;1 也是这个字段的默认值。homePage 使用 navBar,系统便在平行视界状态下保留 Navigation 的主页区域,右侧显示栈顶 NavDestination。两边仍由同一个 Navigation 创建,页面代码继续使用 NavigationMode.Stack。
主页留在左侧以后,未读数、同步状态和分类名称仍然可以刷新。开发者需要保住分类入口的位置,并让选中项跟随操作变化,因此主页适合放置分类列表和筛选入口。邮件正文、附件与回复操作放在右侧,可以把更多空间留给阅读和处理邮件;复杂编辑表单进入左栏,会挤压入口和主要任务各自需要的空间。这是根据邮箱任务作出的布局判断。
冷启动时还没有分类点击,右侧也需要有明确内容。当前 relatedPage 使用 InboxPage,让主页与收件箱建立静态关联。关联页应该在没有动态业务参数时也能展示可理解的内容,邮件 ID 或动态分类参数继续由 Navigation 路由传递。真实收件箱需要请求数据时,由目标页显示加载、空态或错误提示,不能等用户重新点击左栏才开始初始化。
应用中如果存在多个 Navigation,还要确认系统找到的是承载邮箱主页的那个组件。外层全局导航与业务模块内部导航同时存在时,仅凭主页名称容易留下匹配歧义。示例把组件 ID 固定为 mainNavigation,再让 homeNavigationId 使用相同字符串;调整页面关系时可以直接沿着这个 ID 找到组件,不依赖它出现的顺序。
下面的组件属性来自完整示例,保留了普通 Stack 模式。核对 ID 后,再检查 EasyGo 中的关联是否指向同一主页:
.id('mainNavigation')
.mode(NavigationMode.Stack)
.title('邮箱')
.navDestination(this.pageMap)
"navigationSplitOptions": {
"homePage": "navBar",
"relatedPage": "InboxPage",
"homeNavigationId": "mainNavigation",
"mode": 1
}
这组配置把主页来源、初始右栏和组件归属连在一起。需要改默认分类时,可以用下面的对应关系检查改动范围,避免只替换 relatedPage,却留下旧的启动状态。
| 配置 | 本例取值 | 工程职责 |
|---|---|---|
homePage | navBar | 把 Navigation 主页作为左侧区域 |
relatedPage | InboxPage | 建立主页与首个内容页的静态关联 |
homeNavigationId | mainNavigation | 在多个 Navigation 场景中锁定主页所属组件 |
mode | 1 | 使用导航模式的左右区域规则 |
配置能够找到主页,并不包含按钮索引。EasyGo 不知道收件箱对应 selectedIndex 的哪一个值,也不会根据右侧标题推导高亮。当前 Demo 在 aboutToAppear() 中调用 openSection(0, 'InboxPage'),让启动时的索引和路由也指向收件箱。这个调用在窄窗口中同样执行,relatedPage 则负责平行视界中的初始关联。
因此,修改默认分类时,需要一起核对索引、页面名和目标页内容。启动后的预期是收件箱按钮采用选中样式,右侧标题和卡片也都属于收件箱。初始关系一致以后,用户继续点击其他分类,应用还要维持这组对应关系。
二、一次分类点击怎样同时改变两侧
用户点击归档邮件时,按钮将索引 1 和 ArchivePage 交给 openSection()。主页先用索引更新选中样式,再把右栏切到归档页;这两个动作共同完成一次分类选择。把它们放在同一方法中,可以减少某条入口只执行一半的遗漏,也便于统一处理重复点击和埋点。
private openSection(index: number, pageName: string): void {
this.selectedIndex = index
this.pageStack.clear()
this.pageStack.pushPathByName(pageName, null)
}
函数执行后,selectedIndex 变为 1,归档邮件按钮使用选中颜色;原来的收件箱节点被 clear() 清除,pushPathByName() 再把 ArchivePage 放到栈顶,右侧应当显示归档邮件卡片。用户继续点击草稿箱时,函数接收对应索引与 DraftPage,按同样顺序替换右栏根页面。分类入口保持在主页中,变化的是选择状态和目标内容。
这里采用先清空再入栈,与同级切换的返回要求有关。用户从收件箱切到草稿箱后,通常不需要按返回键逐个重放刚才访问过的分类,所以当前示例只保留一个分类页面。具体产品仍要定义返回规则;某些分类历史需要保留时,就要据此调整栈策略。导航模式本身不会替应用决定 clear()、push() 和 pop() 的业务含义。
函数里的 null 也说明了示例范围:三个目标页展示静态邮件卡片,没有请求真实邮件数据。业务工程中的列表加载发生在目标页,完成时间可能晚于高亮和路由切换。右侧请求尚未完成时,应当让加载反馈继续属于当前分类,避免数据变化再次改写选择。
分类数量增加以后,需要防止索引与页面名在调用之前就已经配错。当前三个按钮把参数直接写在 Builder 调用中,仍然容易逐项核对;如果按钮数组、页面映射和默认索引分开维护,新增分类时就可能漏改其中一处。按通用软件工程惯例,可以用稳定分类 ID 组织显示名称、选中索引和目标页面,让一次选择取得同一组数据。颜色、字重和背景形状只呈现选择结果,标题改名不应该改变路由映射。
重复点击还需要单独作出产品选择。示例每次都会清空并重新压入页面,便于观察替换行为;静态设置页可能希望忽略这次点击,数据列表可能希望刷新或回到顶部。正式项目可以比较目标索引与当前索引,再决定执行哪一种动作,但不能仅为了减少路由操作就取消用户预期的刷新。当前代码没有加入忽略重复点击的分支。

读图时沿着同一次分类选择检查两条变化:索引决定左侧高亮,目标页面名决定右侧内容。邮件数据由目标页处理,不会因为这两项同时更新就自动完成加载。
从分类继续进入某封邮件时,用户通常需要返回分类根页面,清空后再入栈就会丢掉这层来路。此时需要保留分类节点并追加详情层级。购物模式让左侧随着层级推进,导航模式保留主页;邮箱当前关注同级切换,因此示例停在分类替换,不加入邮件详情、通知直达或进程恢复。
三、用一条连续操作链找出错位位置
配置和点击函数都有了明确职责,验证时可以从冷启动一路走到窗口缩窄。每一步记录左侧选择与当前目标页,后一步沿用前一步的状态,才能发现只在连续切换中出现的遗漏。
| 操作 | 左栏状态 | 右栏或当前页 | 预期结果 |
|---|---|---|---|
| 冷启动 | 收件箱选中 | InboxPage,收件箱标题与卡片 | 默认索引与初始页面一致 |
| 点击归档邮件 | 归档邮件选中,入口仍在 | ArchivePage | 收件箱节点被替换 |
| 继续点击草稿箱 | 草稿箱选中,入口仍在 | DraftPage | 右栏内容跟随第二次选择 |
| 再次点击草稿箱 | 仍选中草稿箱 | 清空后重新压入 DraftPage | 符合示例的重复点击行为 |
| 缩窄窗口 | 按普通页面显示核对选择 | 普通单页 Navigation | 页面与返回仍符合单页路由规则 |
这条链同时检查初始化、同级替换和窗口回退。宽窗口中要核对标题与卡片,避免页面名称正确而内容仍指向旧分类;缩窄后则按普通单页路径继续检查,不要求两栏仍同时可见。当前 @State 只覆盖本次运行中的分类选择,没有实现账号变化、进程重启或外部入口恢复。

观察左侧分类栏是否持续显示,以及归档邮件、草稿箱的高亮是否与右侧标题和卡片一致。整条点击链和窗口缩窄结果仍需按上表逐步记录,单张图片只能用于核对当时的页面。
发生异常时,可以先按画面把故障分开。左
栏整体消失,需要核对 mode、homePage 和窗口条件;左栏存在而内容没有切换,就检查 openSection() 是否改变当前栈,以及 Builder 的 pageMap 能否创建目标页。右栏正确但高亮错误时,再检查 selectedIndex 的输入和按钮样式。多 Navigation 工程如果保留了错误的主页,还要核对组件 ID 与 homeNavigationId 是否完全一致。
数据失败属于另一条排查分支。收件箱没有邮件时,右侧仍应是 InboxPage,空态继续说明当前分类;网络请求失败后的重试只更新右栏数据。如果错误处理同时清空 NavPathStack,内容页就会退出,原本的数据问题会变成分类与页面错位。这是接入真实邮件数据时需要守住的边界,静态 Demo 没有实现网络请求和重试。
总结
同级分类能够稳定切换,需要一次选择同时给出左栏索引和右栏目标页。导航模式保留主页区域,应用用 openSection() 更新高亮并替换分类根页面,再通过冷启动与连续点击检查两侧是否指向同一分类。
我们的例子采用清空后入栈,适用边界是无需重放历史的同级分类。右栏继续进入详情时,需要保留层级;真实数据加载和进程恢复也需要各自的状态处理,不能由主页常驻推断它们已经完成。
我目前手里还没有可以测试 HarmonyOS 7 的真机,所以相关内容现阶段主要通过 HarmonyOS 7 模拟器进行验证,真机上的系统表现、设备差异和实际体验,后面有条件再继续补测,最终还是以实际设备运行结果为准。
完整代码
Index.ets
@Entry
@Component
struct Index {
@Provide('pageStack')
pageStack: NavPathStack = new NavPathStack()
@State selectedIndex: number = 0
@Builder
pageMap(name: string) {
if (name === 'InboxPage') {
InboxPage()
} else if (name === 'ArchivePage') {
ArchivePage()
} else if (name === 'DraftPage') {
DraftPage()
}
}
aboutToAppear(): void {
this.openSection(0, 'InboxPage')
}
private openSection(index: number, pageName: string): void {
this.selectedIndex = index
this.pageStack.clear()
this.pageStack.pushPathByName(pageName, null)
}
build() {
Navigation(this.pageStack) {
Column({ space: 12 }) {
Text('邮箱分类')
.fontSize(30)
.fontWeight(FontWeight.Bold)
.width('100%')
Text('导航模式让这个分类栏保持在左侧。')
.fontSize(15)
.fontColor('#5F6678')
.lineHeight(22)
.width('100%')
this.sectionItem('收件箱', 0, 'InboxPage')
this.sectionItem('归档邮件', 1, 'ArchivePage')
this.sectionItem('草稿箱', 2, 'DraftPage')
}
.width('100%')
.height('100%')
.padding(24)
.alignItems(HorizontalAlign.Start)
}
.id('mainNavigation')
.mode(NavigationMode.Stack)
.title('邮箱')
.navDestination(this.pageMap)
.width('100%')
.height('100%')
}
@Builder
private sectionItem(label: string, index: number, pageName: string) {
Button(label)
.width('100%')
.height(52)
.fontSize(17)
.fontColor(this.selectedIndex === index ? Color.White : '#28324A')
.backgroundColor(this.selectedIndex === index ? '#5269D8' : '#EEF1F8')
.onClick(() => {
this.openSection(index, pageName)
})
}
}
@Component
struct InboxPage {
build() {
NavDestination() {
Column({ space: 14 }) {
Text('收件箱')
.fontSize(30)
.fontWeight(FontWeight.Bold)
Text('3 封未读邮件')
.fontSize(16)
.fontColor('#5269D8')
MailCard({ sender: '设计团队', subject: '平板界面评审' })
MailCard({ sender: '项目经理', subject: 'API 26 构建记录' })
}
.width('100%')
.height('100%')
.padding(24)
.alignItems(HorizontalAlign.Start)
}
.title('收件箱')
}
}
@Component
struct ArchivePage {
build() {
NavDestination() {
Column({ space: 14 }) {
Text('归档邮件')
.fontSize(30)
.fontWeight(FontWeight.Bold)
Text('右栏已切换,左栏分类和选中状态继续保留。')
.fontSize(16)
.fontColor('#5F6678')
MailCard({ sender: '研发团队', subject: '上周构建结果' })
}
.width('100%')
.height('100%')
.padding(24)
.alignItems(HorizontalAlign.Start)
}
.title('归档邮件')
}
}
@Component
struct DraftPage {
build() {
NavDestination() {
Column({ space: 14 }) {
Text('草稿箱')
.fontSize(30)
.fontWeight(FontWeight.Bold)
Text('右栏展示当前分类内容。')
.fontSize(16)
.fontColor('#5F6678')
MailCard({ sender: '未发送', subject: '折叠屏补测计划' })
}
.width('100%')
.height('100%')
.padding(24)
.alignItems(HorizontalAlign.Start)
}
.title('草稿箱')
}
}
@Component
struct MailCard {
sender: string = ''
subject: string = ''
build() {
Column({ space: 8 }) {
Text(this.sender)
.fontSize(15)
.fontColor('#667085')
.width('100%')
Text(this.subject)
.fontSize(18)
.fontWeight(FontWeight.Medium)
.width('100%')
}
.width('100%')
.padding(18)
.backgroundColor('#F2F4FA')
.borderRadius(16)
.alignItems(HorizontalAlign.Start)
}
}
module.json5
{
"module": {
"name": "entry",
"type": "entry",
"description": "$string:module_desc",
"mainElement": "EntryAbility",
"deviceTypes": [
"phone",
"tablet",
"2in1"
],
"deliveryWithInstall": true,
"installationFree": false,
"easyGo": "$profile:easy_go",
"pages": "$profile:main_pages",
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"description": "$string:EntryAbility_desc",
"icon": "$media:layered_image",
"label": "$string:EntryAbility_label",
"startWindowIcon": "$media:startIcon",
"startWindowBackground": "$color:start_window_background",
"exported": true,
"skills": [
{
"entities": [
"entity.system.home"
],
"actions": [
"ohos.want.action.home"
]
}
]
}
],
"extensionAbilities": [
{
"name": "EntryBackupAbility",
"srcEntry": "./ets/entrybackupability/EntryBackupAbility.ets",
"type": "backup",
"exported": false,
"metadata": [
{
"name": "ohos.extension.backup",
"resource": "$profile:backup_config"
}
]
}
]
}
}
easy_go.json
{
"common": {
"displayModeOptions": {
"wideWindowMode": "navigationSplit",
"squareWindowMode": "navigationSplit",
"navigationSplitOptions": {
"homePage": "navBar",
"relatedPage": "InboxPage",
"homeNavigationId": "mainNavigation",
"mode": 1
}
}
}
}
更多推荐


所有评论(0)