HarmonyOS 7 TreeViewV2实战:康复教育目录如何把点击与已读回执分开
分析:能展开的树,不等于可审计的教育流程
康复教育不是一列平铺的文章。用户先落到「术后第1-2周」这样的阶段,再找到踝泵训练这样的动作,最后要区分「怎么做的步骤」和「什么时候该停」两份内容。这三段隶属是真实的,用缩进和空行伪造层级,换一版计划就散了。
TreeViewV2 是 API 26 才有的分层列表。HarmonyOS 26.0.0 新增和增强特性把它列在 ArkUI 增强项里,组件参考写明它从 26.0.0 起支持、仅 Stage 模型可用,节点由 TreeControllerV2 维护,点击从 TreeListenerV2 上报。
麻烦的是组件不知道「已读」是什么。展开一个阶段、点一下动作目录,都会走进同一个点击回调,回调里只带一个 currentNodeId,连标题和类型都没有。页面要是在这个回调里顺手记账,用户只是想看看结构,账上就多出一条「读完了」。
所以这页只做一件事:组件管层级和展开,页面管「这一次点击算不算读过」。两件事混在一起,后面所有统计都不作数。
功能描述:四类节点、两种身份、一条显式确认
目录里只有四类节点。阶段和动作是目录,负责撑起层级;步骤和注意事项是叶子,是真正要人读的内容。目录随便点,叶子点开只显示内容,也还不算读。
每个节点同时带两套身份:组件认识的数字 ID,和业务认识的稳定编号。数字 ID 只在这一棵树的生命周期里有效,重建一次就可能对不上;稳定编号配上计划修订、内容修订和内容摘要,才是回执该写下的东西。
已读不是点击的副作用,而是一个单独的动作。用户在底部按「这条我看过了」,页面才进检查:选中的是不是叶子、计划修订是不是当前这一版、内容修订和摘要齐不齐、这条是不是已经记过。任何一项不成立都停在原地,不碰账本。
历史内容没有隐藏。目录底部留着「以前那次安排」,展开能看到旧版步骤,正文也直说它是旧版。看它不产生当前计划的回执——回看是追溯,不是签收。

· 展开目录与签收已读走两条不同的判定链:回调只提供节点 ID,回执要过五道检查(示意图)。
案例:从展开到签收,四条路
展开目录。 点阶段行右边的箭头,子节点展开,上方卡片换成这条目录的说明,页头计数不动。实测确认箭头和整行都会触发点击回调,一次点击正好一条事件,带回来的就是被点那条目录的 ID。
打开叶子。 展开动作后点「怎么做的步骤」,卡片显示正文和这一版的内容修订号。页头仍停在「还没记下看过哪几条」,下面提醒「这条已经打开,看完再点下面的按钮」。打开、读完、签收,是三个状态。
签收一条。 按「这条我看过了」,账本写入一条回执,页头变成「已经记下 1 条」,反馈补上「记下了1条,共2条」。同一动作下的两份内容都签完,反馈改成「这一组都记下了」。
该拦的四种情况。 选中目录再签收,得到「目录不用记,点开里面那条内容再记」;同一条再点一次,得到「这条刚才已经记过了,不用重复记」;选中旧版内容,得到「这是以前那次安排里的内容,不能顶替这一次的已读」;节点在本地查不到对应内容时,页面停在「这条没有可记的内容」。四条都不放宽成成功。
退出页面再进来,页头计数还在:账本挂在页面之外的静态数组上,重建树时把它读出来。进程被杀掉就一起没了,这一点在正文里说明白,不假装是持久化。
实测里修掉的两个问题
第一个是卡片高度。上方预览卡随内容变长,整棵树就跟着往下走。1080×1920 的模拟器上量到的位移是 92 像素:双击叶子时第二下正好落到父目录行,日志里出现 ['111','110'] 两条事件。把标题限一行、正文固定三行高、反馈固定两行之后,同一次双击变成 ['111','111'],行位置一次没动。
第二个是行文案。叶子行不渲染二级标题,「已记下」写在叶子上根本没有位置。refreshNode 是声明里给的更新入口,签名只有父节点 ID 和两段副标题,可是它认的是组件内部的「当前节点」,普通点击并不设置它。按叶子写一版、按目录写一版,模拟器上都没改到屏幕。
最后放弃了行级读数:目录行的副标题保持静态,进度只出现在页头和按钮反馈里。少了一个装饰,也少了一个会说谎的界面。
运行截图
以下九张来自同一台模拟器上的同一份构建,逐帧按屏上文本与节点属性核对后再截图。「术后第1-2周」「踝泵训练」这类内容是软件流程夹具,不构成康复处方。

· 康复知识-进入页面只有三行阶段,卡片「还没点开内容」,页头「还没记下看过哪几条」。

· 康复知识-展开第1-2周和踝泵训练后是三层结构,目录行带箭头和副标题,叶子行只有标题。

· 康复知识-选中「踝泵训练」这条目录再按签收,反馈「目录不用记,点开里面那条内容再记」,计数仍为 0。

· 康复知识-点开「怎么做的步骤」,卡片出现正文与修订号,页头计数不变,只提示「这条已经打开」。

· 康复知识-按「这条我看过了」后页头「已经记下 1 条」,反馈写明「记下了1条,共2条」。

· 康复知识-同一条再签一次,反馈「这条刚才已经记过了,不用重复记」,计数仍是 1。

· 康复知识-展开「以前那次安排」并签收旧版步骤,反馈「不能顶替这一次的已读」,页头计数不动。

· 康复知识-步骤和该停的时机都签完,反馈改口为「这一组都记下了」,页头「已经记下 2 条」。

· 康复知识-退回首页再进来,树重新建好,页头仍是「已经记下 1 条」,目录行副标题回到静态文案。
代码片段:树只报点击,页面决定能不能签
建树一次做完再收尾。声明里写明 addNode 只用于初始化、最多 50 层,buildDone 必须在所有节点加完之后调用,漏掉它层级不会生效。
aboutToAppear(): void {
if (!this.listenerAttached) {
this.listener.onNodeClick(this.nodeClickCallback);
this.listenerAttached = true;
}
if (!this.initialized) {
this.nodes.forEach((node: RehabEducationNode) => {
this.controller.addNode({
parentNodeId: node.parentNodeId,
currentNodeId: node.nodeId,
isFolder: !this.isLeaf(node),
primaryTitle: node.title,
secondaryTitle: this.subtitleOf(node)
});
});
this.controller.buildDone();
this.initialized = true;
}
}
点击回调只认 ID。查不到映射就停住,不猜相邻节点,也不在这里写账。
private nodeClickCallback: (event: CallbackParamV2) => void =
(event: CallbackParamV2): void => {
const node: RehabEducationNode | undefined = this.findNode(event.currentNodeId);
if (node === undefined) {
this.selectedTitle = '这条还没对上具体内容';
this.selectedDetail = '树上点到的这一条在本机没有对应的内容,先别记它。';
this.feedback = '已挡住:这条没有可记的内容。';
return;
}
this.selectedNodeId = node.nodeId;
this.selectedTitle = this.rowTitle(node);
this.selectedDetail = node.detail;
this.feedback = this.isLeaf(node) ?
'这条已经打开,看完再点下面的按钮。' :
'这是目录,只能展开收起,里面的单条内容才要记。';
};
签收时按顺序过检查,全部通过才让账本写入。账本自己再按「业务编号 + 内容修订 + 摘要」查一次重,返回 undefined 就说明被拒了。
private acknowledgeSelected(): void {
const node: RehabEducationNode | undefined = this.findNode(this.selectedNodeId);
if (node === undefined) { this.feedback = '已挡住:这条没有可记的内容。'; return; }
if (!this.isLeaf(node)) { this.feedback = '目录不用记,点开里面那条内容再记。'; return; }
if (node.planRevision !== this.activePlanRevision) {
this.feedback = '这是以前那次安排里的内容,不能顶替这一次的已读。'; return;
}
if (node.contentRevision.length === 0 || node.contentDigest.length === 0) {
this.feedback = '这条内容还没配全,暂时不能记。'; return;
}
const receipt: EducationReadReceipt | undefined = RehabEducationLedger.acknowledge(node);
if (receipt === undefined) {
this.feedback = '这条刚才已经记过了,不用重复记。'; return;
}
this.readCount = RehabEducationLedger.count();
const progress: string = this.progressOf(node);
this.feedback = progress.length === 0 ?
`已经记下:${this.rowTitle(node)}。` : `已经记下:${this.rowTitle(node)}。${progress}`;
}
总结
TreeViewV2 给的是层级、控制器和一条只带节点 ID 的点击事件,它撑不起「谁读过什么」。这个案例把展开目录、打开叶子、签收已读拆成三个状态,回执绑定稳定业务编号、计划修订、内容修订和摘要,目录、旧版和重复操作一律失败关闭。
两处问题都是量出来的,不是猜的:预览卡变高会把树顶走 92 像素,双击的第二下就落到别的行;refreshNode 在没有内部选中节点的页面上什么也改不动。修完以后行位置稳定,读数只放在页头和反馈里。
还没做的部分写在这里:账本在内存里,进程重启即清零;屏幕朗读的实际播报、长按连续点击的手感、以及「同一组剩几条」的文案在更窄屏幕上的折行,都还没有验证。
官方资料
更多推荐



所有评论(0)