HarmonyOS Navigation × Node.js:平行视界筛选后详情回退【鸿蒙心迹】
技术栈:HarmonyOS ArkUI Navigation、NavPathStack、NavigationMode.Split、ArkTS @State、Node.js 22(离线回归)
Demo:SplitFilterGuard 任务编号:SF-404
一个很容易被忽略的右侧空白
我在整理一套文档工作台的双栏交互时,发现一个问题很像偶发的页面白屏。左侧原来有“全部、设计、研发、法务”四种筛选,右侧显示当前文档的详情。用户先点中研发计划 DOC-42,随后切到“设计”;左边列表已经只剩设计文档,右边却仍然挂着 DOC-42。进一步切到“法务”时左边完全为空,右边还保留上一次文档,给人的感觉是应用把筛选操作丢了。
问题并不在页面有没有重绘,而在列表数据集合与详情业务标识没有共同的有效性判断。我以前喜欢直接记住某个数组下标,觉得左右两栏不过是宽窄不同的容器。数据一变化,这种写法马上露出破绽:同一个下标可以指向别的文档,也可能已经超出数组边界。即使换成稳定 ID,如果没有在每次筛选后确认 ID 仍属于可见集合,右侧照样会悬空。

先把“回退”说清楚
我没有把这篇写成平行视界系统能力的接入教程。这里讨论的是应用自己的文档选择规则,适用于 Navigation 分栏样式,也适用于手机上的列表到详情跳转。工程约定很明确:当前 ID 仍出现在筛选结果里,继续保持;ID 不在结果里而结果非空,选择结果中的第一份文档;结果本身为空,清空 ID 并展示空态。以后用户重新回到“全部”,不能凭空恢复早已失效的旧 ID,而是按当前规则重新确定详情。
这套策略不是唯一正确答案。有的邮箱会保留已经打开的邮件,有的后台页面希望筛选器只影响左边。这里选择“右侧详情属于当前筛选结果”作为产品合同,是为了让操作结果可以复现、可以讨论。真正的重点是先选定合同,再让代码按合同执行,而不是让路由栈自行猜测用户意图。
在 Demo 中只用三条示例数据:DOC-41 是设计类交互方案,DOC-42 是研发计划,DOC-43 是设计规范。初始 ID 为 DOC-42,依次施加“全部→设计→法务→全部”四个筛选。第一步应 KEEP DOC-42;第二步应 FALLBACK DOC-41;第三步应 EMPTY;第四步因为上一步已经清空选中态,应重新 FALLBACK DOC-41。最终统计就是 KEEP 1 / FALLBACK 2 / EMPTY 1,任何一次得到不同结果,都应定位具体状态转移。
在 Node.js 里先固定状态转移
我把规则从 UI 回调中抽走,做成纯函数工厂。这样不需要 DevEco Studio 也能跑同一份构造数据。下面的代码只接收文档数组、原始选中 ID 与筛选条件,返回可见 ID、当前选中 ID 和转移原因。代码特别区分“没有匹配项”和“有匹配项但原 ID 失效”,是为了避免把 EMPTY 伪装成 FALLBACK。
function makeGate(items, startingId) {
let selected = startingId;
return (category) => {
const visible = category === '全部'
? items : items.filter((item) => item.category === category);
let outcome;
if (visible.length === 0) {
selected = null;
outcome = 'EMPTY';
} else if (selected && visible.some((item) => item.id === selected)) {
outcome = 'KEEP';
} else {
selected = visible[0].id;
outcome = 'FALLBACK';
}
return { category, outcome, selectedId: selected,
visibleIds: visible.map((item) => item.id) };
};
}
这里的 visible[0] 带有一个前提:列表排序策略必须稳定。业务如果按照更新时间倒序展示,就要在筛选前由数据层完成排序;如果排序受服务端更新影响,回退结果发生变化是数据规则的变化,而不是 Navigation 的问题。对于需要还原用户阅读现场的产品,可以把最近访问 ID 列表引入候选顺序,但它仍然必须经过当前可见集合校验。
我还刻意使用 null 表示没有详情,而不是空字符串或一个“临时默认文档”。业务层一旦需要判断是否可以展示编辑按钮,null 的含义最明确。右栏在 EMPTY 状态只放置“当前条件没有可查看的文档”,不应该渲染此前记录的标题、图片或时间。这样也不会把不可见文档的内容残留到导出截图中。
固定四步轨迹,而不是手工点到碰巧正确
本轮随包的 fixtures/docs.json 保存三条文档与筛选序列;tools/filter-trace.mjs 读取它并把详细记录写入 reports/filter-report.json。下面摘录回归入口的关键部分:每次调用状态门禁后都记录当前选择,最后统计三种动作。Node.js 的测试只是业务逻辑层验证,不能替代跨形态 UI 测试。
const apply = makeGate(documents, selectedId);
const steps = filters.map((category) => apply(category));
const summary = {
KEEP: steps.filter((s) => s.outcome === 'KEEP').length,
FALLBACK: steps.filter((s) => s.outcome === 'FALLBACK').length,
EMPTY: steps.filter((s) => s.outcome === 'EMPTY').length
};
console.log(`SF-404 summary KEEP=${summary.KEEP} ` +
`FALLBACK=${summary.FALLBACK} EMPTY=${summary.EMPTY}`);
我会特别看第二步与第三步是否互相污染。第二步回退到 DOC-41 后,“法务”没有任何匹配记录,此时选中态必须真的清掉。如果仅给右侧贴一层“空列表”遮罩,而没有同步模型里的 ID,第四步就可能再次读到隐藏的 DOC-41;更复杂的业务中甚至可能沿用旧权限缓存。让状态变更落实在模型里,才能避免这种看起来没问题的假空态。
到 ArkUI 里,路由只负责承载,不负责挑选业务 ID
华为的 Navigation 文档说明,组件提供 Stack、Split、Auto 等布局模式,分栏场景常用于宽屏设备;NavPathStack 是其页面导航控制器。这里利用这些能力承载左右区域,但选中 ID 的有效性由应用数据模型决定。一个实际工程可把 SelectionGate 写成纯 ArkTS 类,再将返回结果同步到 @State;如下片段只是页面骨架,展示数据与导航的边界,并不声称已经在某台折叠屏上编译通过。
@Entry
@Component
struct Index {
private stack: NavPathStack = new NavPathStack();
@State filter: string = '设计';
@State selectedId: string = 'DOC-41';
@State visibleIds: string[] = ['DOC-41', 'DOC-43'];
@Builder
detailPage(name: string) {
if (name === 'DocumentDetail') {
Text(this.selectedId.length > 0 ? this.selectedId : '暂无可查看文档');
}
}
build() {
Navigation(this.stack) {
Column() {
Text('筛选:' + this.filter);
List() {
ForEach(this.visibleIds, (id: string) => {
ListItem() {
Text(id + (id === this.selectedId ? ' · 已选中' : ''));
}.onClick(() => { this.selectedId = id; });
}, (id: string) => id)
}
}
}
.mode(NavigationMode.Split)
.navDestination(this.detailPage)
}
}

这里还有一个常被混淆的概念:改变 selectedId 不等于修改 Navigation 的栈结构。真正的项目中,详情 NavDestination 的参数通常也保存文档 ID。数据筛选导致当前文档失效时,需要统一更新选中模型和目标详情参数;不要一边直接修改 Text,另一边保留旧路由参数。否则页面重建、返回或者从外部链接再次进入时,旧 ID 会重新出现。具体是替换路径、回退到占位页,还是保持同一个详情目的地并刷新参数,应该由项目的既有导航结构决定。
我倾向于把“集合校验”放在 ViewModel,把“展示哪个详情组件”放在 NavDestination 映射中。这样既能在 Node.js 中验证规则,也方便在 ArkTS 层添加独立测试。右栏展示的是可见列表里经过核验的 ID,不读取 items[currentIndex] 这样的临时下标,更不直接使用筛选前的缓存对象。
检查点放在发生变化的边界
复盘时我会分别制造四类变化:用户改变筛选条件;服务端刷新让已选中记录从结果中消失;文档权限调整使当前 ID 不再可见;窗口从单栏切回双栏。前两类影响的是数据集合,第三类影响访问边界,第四类只应该影响展示方式。如果窗口模式切换本身让选中 ID 变化,说明我们把布局生命周期误当成了业务数据事件。
日志至少记任务号、筛选条件、旧 ID、新 ID、转移原因及可见数量。生产日志不应附带完整文档正文,尤其不要输出业务敏感标题。对空数据场景还需关闭“编辑、分享、删除”等依赖详情对象的按钮;一个灰色空面板不是资源权限校验。收到异步筛选结果时,最好结合当前查询版本号,避免较早的网络响应覆盖较新的用户选择。本篇不实现网络竞态门禁,以免与先前关于旧页回写的文章重复。
预期结果与能力边界
本次真实执行的是 Node.js 22 离线脚本,得到:全部 KEEP DOC-42、设计 FALLBACK DOC-41、法务 EMPTY -、全部 FALLBACK DOC-41,汇总 KEEP=1 FALLBACK=2 EMPTY=1。

遇到异常数据时,模型应拒绝重复 ID、缺少 category 的文档,避免不稳定排序;读取远端失败时保留错误状态,不要直接把网络错误解释成“法务下没有文档”。这些检查在本轮只作为工程扩展点,未编造 SDK 内部行为。用户主动退出详情、页面被系统回收、应用从后台恢复,也应与一次普通筛选操作分别记录,否则统计里的 FALLBACK 会被业务外事件冲淡。
这次最有价值的改动不在界面上,而是把右侧详情的合法性从“路由栈里有页面”改成“当前集合里有对应业务 ID”。当产品继续接入折叠屏、自适应 Navigation 或双栏阅读时,可以保持同一条验证规则;布局怎么变化属于容器问题,文档能不能继续显示属于数据合同问题。
官方资料与核对范围
- 华为《Navigation分栏开发》(页面更新:2026-09-09):https://developer.huawei.com/consumer/cn/doc/doccenter-capabilities/arkts-navigation-split-mode
- 华为 FAQ《Navigation在分栏模式下页面有大量留白》(页面更新:2026-06-26):https://developer.huawei.com/consumer/cn/doc/doccenter-dev-faq/faqs-arkui-1242
- 华为《NavDestination》生命周期相关资料(页面更新:2026-09-08):https://developer.huawei.com/consumer/en/doc/harmonyos-guides/arkts-navigation-navdestination
资料用于确认 Navigation / NavPathStack / 分栏模式的职责;本文 KEEP、FALLBACK、EMPTY、样本数及选择策略均为项目自定义规则,
更多推荐



所有评论(0)