HarmonyOS 7 PopupV2:中止训练前的确认框,为什么还要再核对一次
分析:确认框里的数字,从打开那一刻起就可能过期
康复训练做到一半,用户按了中止。页面要做的事不复杂:把已经做完的项目留下,把不舒服的原因带走,没做的项目不要记成失败。最容易出问题的地方不在文案,而在"已完成 2 项"这个数字是怎么来的。
如果确认框只负责显示,用户点确认时再重新读一次页面,就会封存一份他根本没看过的内容;如果确认框打开时算一份,提交时照抄这份旧内容,框开着期间发生的任何变化又会被无声覆盖。两条路都不对,区别只是把错误留在了哪一侧。
PopupV2 是 API 26 新增的确认类组件。它是一个 @Builder,接收 PopupV2InitInfo,可以设置标题、必填消息、关闭按钮和关闭回调,操作按钮最多两个,还能指定最大宽度和布局方向。它负责把内容显示出来,不负责冻结数据、拦截重复点击,也不负责记录中止这一步到底发生了什么。
所以这里的做法是把两件事拆开:PopupV2 只显示一份打开时冻结的摘要,真正决定能不能提交的是页面里的普通函数。用户在框里看到的,和他最终确认的,必须是同一份内容;对不上就不允许提交。
功能描述:打开时冻结,提交时比对
页面 TrainingAbortConfirmation 用一组 @Local 状态存四件事:已完成项目、不舒服的原因、打开确认框时冻结下来的摘要,以及这次训练是否已经中止。
点击「中止本次训练」时,页面先做两道检查:已经中止过就直接返回,一项原因都没记录也不放行。不舒服的原因是用户自己说出来的感受,不能替他猜一个默认值,也不该用"未填写"混进后续复核里。
检查通过后,页面把当前已完成项目和原因拼成字符串,连同两项数量一起存进 frozenCompleted、frozenReasons、frozenCount、frozenReasonCount,然后才把 showConfirm 置为真。确认框里的那句话只读这几个冻结字段,背景状态再怎么变,框里的内容不动。
用户在框里点「确认中止」时,confirmAbort() 并不直接采信刚才那份摘要,而是把冻结值和当前值重新比一次。已完成项目或原因只要有一边对不上,就关掉旧框,提示"情况有变化,确认内容已经过期",让人重新核对。只有两边完全一致,才把 stopped 置为真并给出中止摘要。
框里的退出方式有三种:右上角关闭、返回核对、确认中止。前两种都只改变框的可见性,完成记录和原因原样保留,也不会写下任何中止结果;只有确认才进入提交判断。组件最多给两个操作按钮,所以关闭按钮单独承担"先退出去看看"这条路,不需要再挤第三个按钮进去。
中止之后,页面上同一组操作全部转成只读:再点补记完成会被拒绝,清空或撤销原因也会被拒绝,状态文字停在"已中止,等待治疗师复核"。这条限制和确认框无关,它写在每个按钮的处理函数里,因为用户完全可能在中止之后继续点页面上的其他东西。
案例:确认框开着的时候,设备又回传了一条完成记录
初始状态是今天上午四项训练完成了两项:踝泵、股四头肌等长收缩,还剩脚跟滑动和坐姿伸膝。原因卡里记着"头晕、疼痛增加",页面底部说明训练还在进行,随时可以中止。
点开「中止本次训练」,确认框写明保留 2 项已完成记录、保存 2 项不舒服的原因,并说明中止后需要治疗师复核。这一刻框里的数字和页面上的数字一致,用户看到的确实是当前状态。
为了把并发这条路径真的走出来,页面在打开确认框后启动一个 4 秒的定时器,模拟训练设备回传一条完成记录:脚跟滑动。框还开着,背景里的已完成数量已经从 2 变成 3,但框内文字仍然是 2。
这时点「确认中止」,页面拒绝提交,回到训练页并提示确认内容已经过期。截图里能看到已完成 3 项、原因仍是两项、训练状态仍是进行中,没有任何中止结果被写下去——旧摘要没有生效,新事实也没有被当成中止依据。
重新打开确认框,这一次冻结的是 3 项。确认后页面给出完整摘要:3 项完成记录和 2 项不舒服的原因已保存,剩下 1 项不会记为失败。随后点「补记一项完成」,页面明确拒绝,并让用户把确实漏掉的项目在复核时告诉治疗师。
原因这条路同样有边界。把两项原因清空后再点中止,页面不会打开确认框,而是提示先选一项不舒服的原因。清空本身不算危险操作,但拿一份空原因去中止训练,会让后续复核失去判断依据,所以拦在打开之前。
运行截图

· 示意图:确认框只显示冻结摘要,提交时再拿当前状态比一次。

· 已完成 2 项、还剩 2 项,没做的两项不会被记成失败。

· 确认框只读冻结值,底部两个按钮分别对应确认与返回核对。

· 已完成数量变成 3 项,页面提示确认内容已经过期、请重新核对。

· 中止摘要写明 2 项完成、2 项原因已保存,剩下 2 项不记为失败。

· 训练已中止,补记入口不再写入,提示改为在复核时告诉治疗师。

· 原因显示"还没有记录",点中止只提示先选一项原因,不打开确认框。
代码片段:PopupV2 只读冻结值,提交前再比一次
打开确认框时,先把页面上此刻的事实抄进四个冻结字段:
private openConfirmation(): void {
if (this.stopped) {
this.feedback = '这次训练已经中止了。治疗师复核后才能开始新的训练。';
return;
}
if (this.reasonNames.length === 0) {
this.feedback = '先选一项不舒服的原因,再中止训练。';
return;
}
this.frozenCompleted = this.joined(this.completedNames);
this.frozenReasons = this.joined(this.reasonNames);
this.frozenCount = this.completedNames.length;
this.frozenReasonCount = this.reasonNames.length;
this.syncedLate = false;
this.showConfirm = true;
this.feedback = '请确认这次中止。中止后需要治疗师复核。';
setTimeout(() => {
if (this.showConfirm && !this.stopped) {
this.completedNames = this.completedNames.concat(['脚跟滑动']);
this.syncedLate = true;
}
}, 4000);
}
setTimeout 那一段是这一页为了复现并发加的:真实产品里,这条记录来自训练设备或另一台终端的同步。它只改当前事实,不碰冻结字段,所以框上的数字会停在原地。
确认框本身很短,消息里只拼冻结值,按钮只有两个:
PopupV2({
title: '确认中止本次训练',
message: `保留 ${this.frozenCount} 项已完成记录,保存 ${this.frozenReasonCount} 项不舒服的原因。中止后需要治疗师复核,才能开始新的训练。确认期间如果有新的完成记录同步进来,会请你重新核对。`,
showClose: true,
onClose: () => {
this.showConfirm = false;
this.feedback = this.syncedLate
? '训练设备刚同步了一条完成记录:脚跟滑动。请重新核对再中止。'
: '已返回核对,完成记录和原因都还在。';
},
buttons: [{
text: '确认中止',
action: () => { this.confirmAbort(); }
}, {
text: '返回核对',
action: () => { this.showConfirm = false; }
}] as [PopupV2Button | undefined, PopupV2Button | undefined]
})
真正拦住过期确认的是 confirmAbort() 开头的比较。它没有相信框里的文本,而是把冻结值和当前值再对一遍:
private confirmAbort(): void {
if (this.stopped) {
this.showConfirm = false;
this.feedback = '这次训练已经中止过了,不用重复提交。';
return;
}
if (this.frozenCompleted !== this.joined(this.completedNames) ||
this.frozenReasons !== this.joined(this.reasonNames)) {
this.showConfirm = false;
this.feedback = '情况有变化,确认内容已经过期。请重新核对再中止。';
return;
}
this.stopped = true;
this.showConfirm = false;
this.feedback = this.abortSummary();
}
这段代码刻意用产品里的名称做比较,是因为这一页只承载一个会话、一份原因列表。换成真实账本时,比较对象要改成稳定的项目 ID、原因 ID 和各自的修订号:名称会随文案调整而变,修订号才能说明"用户看到的是哪一版"。比较也不能只做一次点击校验,提交还得带幂等键,否则重复回调仍会写出两条中止记录。
总结
PopupV2 能解决的是"确认长什么样":标题、消息、关闭入口、最多两个操作按钮,都由它统一。它解决不了"这份确认还算不算数",所以冻结和比对必须写在页面函数里:打开时抄一份,提交前再核一次,对不上就让人重新看。
中止之后所有入口转只读,是同一套判断的延伸。用户看到的就是他确认的,确认过的不会被后来的记录悄悄改写,中止过的也不会因为多点几下而多出第二条记录。把这三件事分开处理,康复训练这种"事实一直在长"的页面才不会在中止那一刻丢失上下文。
官方资料
更多推荐



所有评论(0)