HarmonyOS 7 CounterV2实战:余量收缩时,下一组那个数字为什么自己变小
分析:计数器守的是输入范围,不是处方
康复训练里「下一组做几次」不是一个自由数字。处方总量 12 次、今天已经做完 4 次,那还能安排的就是 8 次;机构把漏记的 2 次补上,余量立刻掉到 6;用户刚记下「做到第 6 次时头晕」,这一组的安排就得归零,等治疗师重新看过再说。
CounterV2 是 API 26 新加的组件,HarmonyOS 26.0.0 新增和增强特性把它列在 ArkUI 增强项里。本机 SDK 的 @ohos.arkui.advanced.CounterV2.d.ets 给它标了 @since 26.0.0、@stagemodelonly、@crossplatform,CounterV2Type 四个枚举值是 LIST = 0、COMPACT = 1、INLINE = 2、INLINE_DATE = 3;CounterV2Options 里只有 type 必填,数值型走 numberOptions。
声明文件里还有一组默认值值得记住:min 默认 0、max 默认 999、step 默认 1、value 默认 0。不写 max 的计数器不是"没有上限",而是上限 999——对康复处方来说这等于没有闸。
所以这一页的分工很清楚:组件守住"用户能输入什么",业务守住"这件事还成不成立"。余量一变,页面必须自己判断刚保存的草稿还算不算数,不能等组件通知。
实测第一处:加号点到上限就自己灰掉
从 4 开始连点加号,点第五下之后数字停在 8 不动了。dumpLayout 抓到的节点里,CompactAddButton… 的 enabled 已经是 false,减号那侧还是 true。整个过程 onChange 最后一次报回的就是 8,组件没有报过一个越界的 9。
这意味着页面里那句夹取 Math.max(0, Math.min(value, this.maximumAdditional())) 在纯点按的路径上永远不会命中。它不是多余的防御代码,但它的用武之地不在这里——下面那处才是。
减到 0 的时候行为对称,还多给了一条信息:减号按钮 enabled=false,同时它的 description 从「单指双击即可执行,双击并按住可持续减少」变成空字符串,而加号那侧的描述还在。另外这两枚按钮的节点 key 带运行时后缀,同一份构建两次启动分别拿到 CompactSubButton1792712984887 和 CompactSubButton1792713587052,跨启动不稳定,写用例时别把它当常量。
实测第二处:上限收缩,组件会自己夹回来
把数字点到 8(这时上限也是 8),然后按「同步完成进度」——完成量从 4 变成 6,余量从 8 掉到 6。屏上计数器跟着变成 6,回显行变成「下一组 6次」。
组件为什么会知道?为了分清是页面夹的还是组件夹的,把页面里那段"完成量更新后把超界的草稿夹回新上限"的代码删掉,重新构建再走一遍同样的操作:数字照样从 8 掉到 6,而且 onChange 报回了一个 6。也就是说当新的 max 小于当前值时,组件自己夹到新上限,并把夹完的值通过回调送回来。
紧接着暴露出一个真问题。页面在同步进度时先写好了一句「已同步最新进度:今日已完成6次,下一组最多6次。」,可组件的回调随后落到同一个 feedback 字段上,把它覆盖成「已调整为6次,请重新保存。」。用户看到的是:数字自己变小了,屏上却像他自己刚调过一样,原因一个字都没留下。
改法是把原因留在页面里:收缩发生前先记下「今日已完成6次」,回调发生时合并成「今日已完成6次,下一组最多还能安排6次,请重新保存。」。对照帧也测了——当值仍落在新上限之内(3 次,新上限 6),组件不回叫,屏上说明就停在「已同步最新进度:今日已完成6次,下一组最多6次。」。所以 onChange 不是"值变了就报",它只在组件需要纠正自己时报,页面感知余量变化只能自己算。
功能描述:一个上限函数,三处共用
页面只有四块:动作抬头、今日进度卡、设置下一组卡、状态卡;操作是保存下一组、同步完成进度、结束本次训练三颗按钮。
上限只有一个来源:maximumAdditional() 在活动状态返回 处方总量 − 已完成,停止状态直接返回 0。组件的 max、进度卡上那句「今天还可安排N次」、保存前的二次校验都调它,避免同一个余量在三处算出三个数。
保存下来的是一份草稿快照,除了次数还留下「保存时已完成几次、当时最多可安排几次」,屏上写成一行小字。之所以要留这两个数,是因为「6 次」在完成 4 次时还有 2 次余量,在完成 6 次时已经顶满,只存一个数字没法在补记之后判断它还成不成立。
余量归 0 之后,这一页不再留一个用不动的计数器:设置卡和保存按钮一起从组件树上撤下,换成「今天的次数已排满 / 今天共12次已全部安排完,下一组不再增加次数。」,「同步完成进度」在 12/12 之后禁用。结束本次训练走同一套处理——卡片换成「本次训练已结束」,三颗按钮全部撤下,先前保存的草稿作废。
案例:一份 8 次草稿的三次改口
第一次改口来自上限收缩。停在 8 次、按同步,屏上回显「今日已完成6次,下一组最多还能安排6次,请重新保存。」,加号已经灰掉,状态卡仍是「等待保存」。
第二次来自保存本身。按「保存下一组」之后,状态卡变「下一组已保存」,小字写「保存时已完成4次,当时最多可安排8次。」——注意这行留的是当时的数,之后余量再怎么收缩,这行都不会被就地改写,它是快照不是投影。
第三次来自停止。在保存过一份草稿之后按「结束本次训练」,设置卡整块换成「本次训练已结束 / 下一组的安排已经取消,如需继续请先联系康复治疗师。」,保存、同步、结束三颗按钮一起消失,状态卡回「本次训练已结束,原来保存的下一组已取消。」。这里没有"撤销结束"的开关:停止是要给治疗师回看的记录,恢复得靠一次新的复核,不在原会话上把一个布尔值翻回去。
还有一处兜底说明。保存函数里有一条「当前次数无法保存,请先核对训练状态。」的拒绝分支,本轮从界面路径一次都没触发过——组件已经夹好了值,页面也夹了一遍。它只在直接调用保存函数的路径上才有意义,比如以后接了深链或者自动保存。
运行截图
以下八张来自同一台模拟器上的同一份构建,逐帧按屏上文本与节点属性核对后再截图;「右膝术后第28天」「12 次」都是软件流程夹具,只表达用户当下的观察,不构成康复处方或训练建议。

· 下一组次数-计数器 4 次,进度卡「已完成 4 / 12次」「今天还可安排8次」,状态卡「等待保存」。

· 下一组次数-连点加号停在 8,加号节点 enabled=false,回显「下一组 8次」。

· 下一组次数-减到 0,屏上提示「下一组至少需要1次。」,保存按钮点不动。

· 下一组次数-8 次草稿遇到同步:完成 6、余量 6,计数器夹回 6,加号灰掉,说明写明「今日已完成6次,下一组最多还能安排6次」。

· 下一组次数-保存 6 次草稿,状态卡「下一组已保存」,小字留下「保存时已完成4次,当时最多可安排8次。」。

· 下一组次数-3 次落在新上限 6 之内,组件没有回调,屏上停在「已同步最新进度:今日已完成6次,下一组最多6次。」。

· 下一组次数-12/12 之后计数器与保存按钮撤下,卡片换成「今天的次数已排满」,同步按钮禁用。

· 下一组次数-结束本次训练后设置卡换成「本次训练已结束」,保存的草稿作废。

· 下一组次数-从点到上限、余量收缩夹回到值在范围内的对照顺序,按代码分支画成的流程示意图,不是屏上画面。
代码片段:上限来自领域函数,回调要把原因带上
上限只有一个算法,停止时归零:
private maximumAdditional(): number {
if (this.stopped) {
return 0;
}
return Math.max(this.prescribedTotal - this.completed, 0);
}
计数器只拿到标签、当前值和这一份动态上限:
CounterV2Component({
options: {
type: CounterV2Type.COMPACT,
numberOptions: {
label: '下一组次数',
value: this.repetitions,
min: 0,
max: this.maximumAdditional(),
step: 1,
onChange: (value: number) => { this.onRepetitionsChange(value); }
}
}
}).id('kangban-repetition-counter')
回调里先处理收缩带来的改口——组件夹回的值会盖掉页面刚写的进度说明,所以原因由页面自己带进来:
private onRepetitionsChange(value: number): void {
if (this.stopped) {
this.repetitions = 0;
this.invalidateSaved('本次训练已结束,如需继续请先联系康复治疗师。');
return;
}
const next: number = Math.max(0, Math.min(value, this.maximumAdditional()));
this.repetitions = next;
if (this.shrinkNote.length > 0) {
const note: string = this.shrinkNote;
this.shrinkNote = '';
this.savedDraft = undefined;
this.feedback = `${note},下一组最多还能安排${next}次,请重新保存。`;
return;
}
if (next === 0 && this.maximumAdditional() === 0) {
this.invalidateSaved('今天的次数已经排满,下一组不再增加。');
return;
}
this.invalidateSaved(next > 0 ? `已调整为${next}次,请重新保存。` : '下一组至少需要1次。');
}
进度更新时先判断这次收缩会不会触发组件夹取,再决定屏上怎么说:
private appendTwoCompleted(): void {
if (this.stopped) {
return;
}
this.completed = Math.min(this.completed + 2, this.prescribedTotal);
const nextMaximum: number = this.maximumAdditional();
if (this.repetitions > nextMaximum) {
this.shrinkNote = `今日已完成${this.completed}次`;
this.repetitions = nextMaximum;
} else {
this.shrinkNote = '';
}
this.invalidateSaved(nextMaximum > 0
? `已同步最新进度:今日已完成${this.completed}次,下一组最多${nextMaximum}次。`
: `已同步最新进度:今日已完成${this.completed}次,今天的次数已经排满。`);
}
总结
CounterV2 管输入,页面管事实,这条线在这一次实测里比预想的更细。到上限时组件自己把加号 enabled 置为 false,不会报出越界值;但当新的 max 掉到当前值之下,它会自己夹到新上限、把夹完的值经 onChange 送回来,顺手把页面刚写好的进度说明覆盖掉。反过来,值仍在新上限之内时它一声不响——所以余量变化只能页面自己算,别指望回调通知。
三处改动值得单独记住。收缩前先记下原因,回调发生时合并成一句人话,屏上才不会把组件的调整说成用户的操作;余量归 0 时把计数器和保存按钮一起撤下,留一个点不动的控件只会让人以为是屏幕没反应;草稿保存时把"当时已完成几次、当时最多几次"一起留下,事后才解释得清这份安排为什么作废。
还没测的也说清楚:这轮没做有效 Lint,构建只跑到 assembleHap 并免签名安装到模拟器;屏幕朗读实际怎么念这两枚没有文本的按钮、"双击并按住"的持续增减会不会越过上限、LIST/INLINE/INLINE_DATE 三种形态、真机(非模拟器)表现、以及真实服务端的并发补记与事件幂等都没有观察过。生产版的"补记 2 次"不能写成一句 completed += 2:每个完成事件要带稳定事件 ID、会话 ID 和计划修订,重放同一事件不得再次累加;客户端保存草稿时如果携带的服务端修订已经过期,正确做法是拉回最新完成事实、重算上限、请用户重新确认,而不是把旧数字强行写进去。
官方资料
- HarmonyOS 26.0.0 新增和增强特性:
CounterV2所属版本与能力清单。 - CounterV2 API 参考:组件页;本文的
CounterV2Type枚举值、numberOptions字段与min/max/step/value默认值,按本机 SDK 声明文件@ohos.arkui.advanced.CounterV2.d.ets逐条核对。
更多推荐



所有评论(0)