前言

用户点击草稿箱以后,右侧已经显示草稿,左侧却仍然高亮收件箱。邮箱放进宽窗口时,这种错位会同时出现在眼前:分类栏虽然一直存在,却给出了错误的位置提示。设置目录和知识库章节也会遇到同样的问题,入口需要保留,选中项还要跟着当前内容更新。

平行视界的导航模式可以把主页区域留在左侧,让右侧显示当前路由栈顶页。系统确定两栏位置以后,应用仍要维护分类选择和邮件数据。要解决开头的错位,需要先让主页与默认内容建立正确关系,再让一次分类点击同时改变左栏高亮和右栏路由。

一、冷启动应当显示哪个分类

分类入口要持续位于左侧,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,却留下旧的启动状态。

配置本例取值工程职责
homePagenavBar把 Navigation 主页作为左侧区域
relatedPageInboxPage建立主页与首个内容页的静态关联
homeNavigationIdmainNavigation在多个 Navigation 场景中锁定主页所属组件
mode1使用导航模式的左右区域规则

配置能够找到主页,并不包含按钮索引。EasyGo 不知道收件箱对应 selectedIndex 的哪一个值,也不会根据右侧标题推导高亮。当前 Demo 在 aboutToAppear() 中调用 openSection(0, 'InboxPage'),让启动时的索引和路由也指向收件箱。这个调用在窄窗口中同样执行,relatedPage 则负责平行视界中的初始关联。

因此,修改默认分类时,需要一起核对索引、页面名和目标页内容。启动后的预期是收件箱按钮采用选中样式,右侧标题和卡片也都属于收件箱。初始关系一致以后,用户继续点击其他分类,应用还要维持这组对应关系。

二、一次分类点击怎样同时改变两侧

用户点击归档邮件时,按钮将索引 1ArchivePage 交给 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 只覆盖本次运行中的分类选择,没有实现账号变化、进程重启或外部入口恢复。

观察左侧分类栏是否持续显示,以及归档邮件、草稿箱的高亮是否与右侧标题和卡片一致。整条点击链和窗口缩窄结果仍需按上表逐步记录,单张图片只能用于核对当时的页面。

发生异常时,可以先按画面把故障分开。左

栏整体消失,需要核对 modehomePage 和窗口条件;左栏存在而内容没有切换,就检查 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
      }
    }
  }
}
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐