这次处理的是“宽屏变窄”才会暴露的问题。SplitRouteLab 在展开态使用 Navigation 自适应分栏,左边 Notes,右边 NoteDetail;收拢成 STACK 时,用户应该继续停留在当前详情。

真正的问题出在连续点击和模式切换同时发生时:同一个 note_208 可能重复入栈;收拢后返回两次才回列表;右侧空白时如果把 splitPlaceholder 当成真实路由,又会额外污染返回栈。

所以本文只处理三件事:重复入栈抑制、分栏收拢时详情栈归一、占位页保持“只展示不入栈”。

本次数据:

  • Session:nav_merge_20261001_18
  • Mode Change:SPLIT → STACK
  • Master:Notes
  • Selected:note_208
  • Stack Before:2
  • Stack After:1
  • Duplicate Push Blocked:2
  • Placeholder Used:true
  • Mode Changes:3
  • State:MERGED
  • Last Merge:18:58:24

一、splitPlaceholder 只负责“右侧别空着”,不要把它当成路由页

分栏右侧没有 NavDestination 时,可以用 splitPlaceholder 提供默认 UI。最容易犯的错误,是把占位页也 push 进 NavPathStack;这样收拢成单栏后,它就会变成一个可返回页面。

正确做法是让占位页只存在于 Navigation 的展示层:

@State pathStack: NavPathStack = new NavPathStack()

build() {
  Navigation(this.pathStack) {
    NotesList({
      onSelect: (id: string) => {
        this.selectNote(id)
      }
    })
  }
  .mode(NavigationMode.Auto)
  .navDestination(this.PageMap)
  .splitPlaceholder(() => {
    Column() {
      Text('选择一条笔记查看详情')
        .fontSize(20)
      Text('当前右侧没有已选内容')
        .fontColor('#808080')
    }
  })
}

splitPlaceholder 只解决“分栏右侧别空着”,不对应业务 route name,也不进入返回栈。

二、列表点击先做业务级去重,再交给 NavPathStack

第二个问题是重复点击。系统不会知道两次 pushPathByName('NoteDetail', ...) 在业务上其实是同一条笔记,所以我先用 selectedNoteId + 当前栈内容 做幂等判断:

@State selectedNoteId: string = ''
@State duplicateBlocked: number = 0

private selectNote(noteId: string): void {
  const names = this.pathStack.getAllPathName()
  const detailExists =
    names.includes('NoteDetail')

  if (this.selectedNoteId === noteId &&
      detailExists) {
    this.duplicateBlocked++
    return
  }

  this.selectedNoteId = noteId

  if (detailExists) {
    this.pathStack.removeByName('NoteDetail')
  }

  this.pathStack.pushPathByName(
    'NoteDetail',
    { id: noteId }
  )
}

对这种 Master → 单一 Detail 结构,新的 selection 到来时先移除旧 NoteDetail 再推入新详情。本次两次重复点击被挡掉,所以 Duplicate Push Blocked=2。如果业务允许详情继续跳详情,就必须换成更精细的 route key 策略。

三、SPLIT 收拢成 STACK 时,要做一次路由归一

真正的问题出现在模式切换。收拢成单栏后,如果当前选中 note_208,栈顶只应该保留一个 NoteDetail(note_208)。所以我把“模式变化”转换成业务状态,再统一走一次 merge:

private mergeForStack(): void {
  const names = this.pathStack.getAllPathName()
  const detailCount =
    names.filter(name => name === 'NoteDetail').length

  this.stackBefore = detailCount

  if (detailCount > 1) {
    this.pathStack.removeByName('NoteDetail')
  }

  const afterRemove =
    this.pathStack.getAllPathName()

  if (this.selectedNoteId &&
      !afterRemove.includes('NoteDetail')) {
    this.pathStack.pushPathByName(
      'NoteDetail',
      { id: this.selectedNoteId }
    )
  }

  this.stackAfter =
    this.pathStack.getAllPathName()
      .filter(name => name === 'NoteDetail')
      .length

  this.state = 'MERGED'
}

这次 Stack Before=2,归一之后 Stack After=1。

重点不是 removeByName(),而是模式切换后重新表达业务意图:selection 是 note_208,详情栈就只能留下这一份。

四、窗口变窄时,我不让多个回调同时改栈

折叠或窗口拖动可能连续触发;如果每次都调用 mergeForStack(),路由栈可能一边 remove、一边又被下一轮 push。

所以我给模式收敛加了 generation:

private modeGeneration: number = 0

private onExpectedModeChanged(
  nextMode: 'SPLIT' | 'STACK'
): void {
  const generation = ++this.modeGeneration
  this.modeChanges++

  setTimeout(() => {
    if (generation !== this.modeGeneration) {
      return
    }

    if (nextMode === 'STACK') {
      this.mergeForStack()
    } else {
      this.state = 'SPLIT_READY'
    }
  }, 80)
}

80 ms 只是 Demo 的收敛窗口,用来确保同一轮折叠或窗口拖动里只执行最后一次栈整理。

五、占位页恢复的关键,是“不要恢复路由”

重新进入宽屏且没有 selectedNoteId 时,右侧自然回到 splitPlaceholder。我不会 push 一个 Placeholder 路由,也不会把“上次看见占位页”持久化成路由快照;真正需要持久化的是业务 selection。

六、调试页只展示和“栈是否收敛”有关的数据

工程只保留 NotesHome、NoteDetail、RouteMergeCoordinator 和 NoteSelection。HiLog 输出:

mode=SPLIT placeholder=true

select note_208

duplicate push blocked=1

mode change -> STACK

remove NoteDetail count=2

push final NoteDetail id=note_208

State: MERGING -> MERGED

七、最终结果看的是“返回一次应该去哪”

最终诊断页里 Mode Change=SPLIT → STACK、Stack Before=2、Stack After=1、Selected=note_208。收拢后详情只剩一份,返回一次就应回到 Notes 列表。Placeholder Used=true 只说明空分栏曾展示占位 UI,不代表它进入过路由栈。

八、正式项目还要考虑几个边界

正式项目还要处理 route key 精确合并、单双栏切换协调、deep link 统一入口,以及 selection 失效后的清空。

九、这次修的不是 Navigation,而是“模式变化时业务路由怎么收口”

最终链路是 SPLIT → MERGING → STACK → MERGED。Navigation 管布局,NavPathStack 管真实路由,业务层只回答:当前应该有几个详情页。

Logo

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

更多推荐