ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

HarmonyOS 7 ArkUI:双栏切换弹窗归属与过期回调隔离

HarmonyOS 7 ArkUI:双栏切换弹窗归属与过期回调隔离 工单平板页从单栏切到平行视界双栏时一个“放弃修改”弹窗突然消失更糟的一次弹窗没消失却把刚刚切换到的新工单草稿删了。问题不在弹窗样式而在弹窗究竟属于哪个组件、确认动作绑定的是哪个瞬间的数据。本文复盘 DeskFlow 的修复过程把弹窗提升到稳定宿主用不可变快照冻结命令再用代次隔离过期回调。一、一个只在折叠屏旋转时出现的错删DeskFlow 是内部工单处理 Demo。左侧是工单列表右侧是详情和回复草稿窄屏时两页通过Navigation前后切换展开或平板横屏时启用 Parallel View列表和详情同时可见。测试用例PV-2417的步骤很刁钻编辑工单TK-2048点击列表里的TK-2051弹出“放弃当前草稿”弹窗尚未确认时把设备从半折叠转成展开随后点击“放弃并切换”。旧实现有两个结果。某些设备上详情组件重建绑定在组件内部的CustomDialogController被销毁弹窗直接消失另一些情况下弹窗仍在但确认回调读取的是页面最新的selectedId于是它删掉了TK-2051的草稿而不是发起动作时的TK-2048。这类问题很难从正常点击路径看出来。日志当时只有dialog confirmed没有宿主、源工单、目标工单和布局代次。我把修复目标写成一句话弹窗的视觉生命周期必须属于稳定页面业务命令的数据生命周期必须属于触发瞬间。本次演示统一使用页面名DeskFlow Workbench当前工单TK-2048目标工单TK-2051布局模式DUAL_PANE代次layoutEpoch7。最终状态是DRAFT_PROTECTED草稿 126 字仍然存在。二、先把布局变化和业务选择分开我最初用一个isSplit同时控制双栏布局、详情显隐和弹窗关闭。窗口宽度变化时三个动作被绑在一起组件树重排、详情节点销毁、弹窗控制器释放。问题根源就在这里——布局事实和业务事实不是一回事。当前问题是窗口从单栏变双栏时只能改变承载方式不能顺手改掉selectedTicketId或草稿所有权。下面的状态仓库把两类状态拆开并为每次布局变更递增代次。exporttypePaneModeSINGLE_PANE|DUAL_PANEObservedV2exportclassWorkbenchStore{TracepaneMode:PaneModeSINGLE_PANETraceselectedTicketId:stringTK-2048TracedraftOwnerId:stringTK-2048TracedraftText:stringTracelayoutEpoch:number6applyWindowWidth(widthVp:number):void{constnext:PaneModewidthVp840?DUAL_PANE:SINGLE_PANEif(nextthis.paneMode)returnthis.paneModenextthis.layoutEpoch}hasDirtyDraft():boolean{returnthis.draftText.trim().length0}}当窗口跨过840 vp阈值时只更新paneMode和layoutEpoch。selectedTicketId、draftOwnerId、draftText都保持不变因此 ArkUI 重排组件树不会被误解成用户切换工单。layoutEpoch从 6 变成 7给后续诊断留下明确证据。applyWindowWidth()应由窗口尺寸回调或页面获得的布局约束触发不能在build()中无条件改状态。实际产品要对阈值加迟滞区间避免窗口在 839/840 vp 抖动时反复重排。仓库本身没有系统资源需要释放但窗口监听必须在页面退场时注销否则旧页面仍可能增加代次并触发重复布局。三、弹窗不能住在会被替换的详情组件里旧的DiscardDialog定义在TicketDetail中。单栏进入详情时没有问题切到双栏后系统可能以新的承载结构创建详情节点旧控制器随组件销毁。修复方式不是“延迟 300 毫秒再弹”而是把弹窗宿主提升到DeskFlowWorkbench根组件使列表、详情和单栏导航都只提交弹窗请求。当前问题是弹窗需要一个跨布局稳定的宿主同时又不能直接读取随时变化的页面字段。先定义一次性命令快照。exportinterfaceSwitchTicketCommand{commandId:stringsourceTicketId:stringtargetTicketId:stringdraftOwnerId:stringdraftLength:numbercreatedAtEpoch:number}exportclassDialogCommandBus{privatepending?:SwitchTicketCommandpublish(command:SwitchTicketCommand):void{if(this.pending)returnthis.pendingObject.freeze({...command})}take():SwitchTicketCommand|undefined{constvaluethis.pendingthis.pendingundefinedreturnvalue}clear():void{this.pendingundefined}}用户在TK-2048有 126 字草稿时点击TK-2051命令立即冻结源工单、目标工单、草稿归属和布局代次都不会再跟着页面变化。publish()在已有命令时拒绝覆盖避免快速点击多个列表项让同一个弹窗换了目标。这个总线只保存一个待处理命令适合阻塞式确认弹窗正式项目如果有队列型消息应区分普通提示和危险操作危险操作不应自动排队。take()会消费命令确认和取消只能有一个分支获得它。页面销毁时调用clear()避免旧 Ability 恢复后重放。这里没有网络资源但存在重复发布风险所以 UI 点击和命令总线要同时防重。四、确认回调先验身份再碰草稿把弹窗移到根组件后弹窗不再消失但还不能直接执行删除。用户可能在系统动画期间通过键盘、深链或其他入口改变选择布局也可能完成两次切换。确认动作必须检查待处理命令仍然存在、草稿归属仍是源工单、当前草稿长度没有发生变化。当前问题是“确认”按钮不能相信闭包里的页面当前值只能消费快照并逐项验真。下面的方法把拒绝原因写成可诊断状态。exporttypeGuardResultSWITCHED|DRAFT_PROTECTED|NO_COMMANDexportclassTicketSwitchCoordinator{constructor(privatestore:WorkbenchStore,privatebus:DialogCommandBus){}confirmDiscard():GuardResult{constcmdthis.bus.take()if(!cmd)returnNO_COMMANDconstownerChangedthis.store.draftOwnerId!cmd.draftOwnerIdconstcontentChangedthis.store.draftText.length!cmd.draftLengthif(ownerChanged||contentChanged){returnDRAFT_PROTECTED}this.store.draftTextthis.store.selectedTicketIdcmd.targetTicketIdthis.store.draftOwnerIdcmd.targetTicketIdreturnSWITCHED}}在PV-2417里旋转期间输入法补回了一个字符draftLength与快照不一致所以结果是DRAFT_PROTECTED。页面保留TK-2048的 126 字草稿并提示“草稿已变化请重新确认”。这看起来比直接切换多一步却避免了真正的数据损失。为什么没有强制要求createdAtEpoch layoutEpoch因为布局变化本身不是业务冲突只要草稿身份和内容没变用户确认仍然可以生效。代次用于日志和策略扩展而不是粗暴拒绝所有旋转后的动作。正式项目可再加入草稿版本号或内容哈希比字符长度更可靠。本 Demo 用长度是为了让状态清晰可视。五、根组件只负责主持不负责猜根组件持有CustomDialogController列表和详情通过回调请求切换。弹窗关闭时必须清理命令页面离开时必须关闭控制器避免 WindowStage 已变化后继续回调 UI。当前问题是把弹窗控制器和业务协调器接起来同时保证连续点击只打开一次。EntryComponentV2struct DeskFlowWorkbench{Localstore:WorkbenchStorenewWorkbenchStore()privatebus:DialogCommandBusnewDialogCommandBus()privateswitching:booleanfalseprivatedialog?:CustomDialogControlleraboutToAppear():void{this.dialognewCustomDialogController({builder:(){this.DiscardDraftDialog()},autoCancel:false},this.getUIContext())}privaterequestSwitch(targetId:string):void{if(this.switching||targetIdthis.store.selectedTicketId)returnif(!this.store.hasDirtyDraft()){this.store.selectedTicketIdtargetIdreturn}this.switchingtruethis.bus.publish({commandId:CMD-PV-2417,sourceTicketId:this.store.selectedTicketId,targetTicketId:targetId,draftOwnerId:this.store.draftOwnerId,draftLength:this.store.draftText.length,createdAtEpoch:this.store.layoutEpoch})this.dialog?.open()}aboutToDisappear():void{this.bus.clear()this.dialog?.close()this.dialogundefined}}requestSwitch()在列表点击、键盘上下选择和单栏返回前都能复用。当前已有草稿时状态先进入CONFIRMING而不是立即改selectedTicketId。弹窗确认或取消后要把switching恢复为false这部分在自定义弹窗按钮回调里完成。CustomDialogController的创建位置跟随根页面避免详情组件在单双栏切换时销毁它。不同 API 版本的构造参数可能有差异正式工程应以当前 SDK 为准核心原则是控制器归稳定 UIContext 所有并在退场时关闭。不要把控制器放进全局单例它引用 UIContext跨窗口复用容易造成无效上下文和资源泄漏。六、日志把“看起来随机”变成一条时间线修复前我只能复述“旋转后偶现错删”。修复后HiLog 会连续打印PV-2417 commandCMD-PV-2417 TK-2048 - TK-2051、Pane SINGLE_PANE - DUAL_PANE epoch7 width920vp、Guard DRAFT_PROTECTED draft126。三条日志刚好对应命令创建、布局变化、最终裁决。项目目录也围绕这条时间线拆分pages/DeskFlowWorkbench.ets是稳定宿主components/TicketList.ets与TicketDetail.ets只是视图dialog/DialogCommandBus.ets保存一次性命令model/WorkbenchStore.ets管理业务状态service/TicketSwitchCoordinator.ets负责验真。DevEco Studio 图中右侧模拟器保持双栏弹窗位于整个工作台之上不属于任一栏中间代码显示confirmDiscard()的身份检查底部 HiLog 的layoutEpoch7、DRAFT_PROTECTED与正文一致。这个布局也能直观看出为何弹窗宿主必须在两栏之上。七、最终运行结果不是“成功切换”这次 Demo 最值得展示的结果恰恰是系统拒绝了危险动作。手机竖屏恢复单栏后页面仍显示TK-2048126 字草稿完整状态卡写着DRAFT_PROTECTED目标TK-2051被保留为待切换项用户可重新发起确认。这张图证明三件事布局已经从双栏切回单栏命令仍有可追踪 IDCMD-PV-2417草稿没有因为过期确认被清空。产品上可以把红色技术批注去掉但调试版本保留这些字段很有用。边界上还要处理系统返回键和应用退后台。如果弹窗打开时按返回应走取消分支并消费命令如果 Ability 进入后台危险命令不要持久化回来后让用户重新触发。草稿本身可以持久化弹窗意图不应该持久化。多窗口同时打开同一工单时则需要服务层草稿版本号单页面的长度校验不再足够。八、我最后留下的三条规则第一Parallel View 改变的是页面承载不应顺便改变业务选择。第二弹窗属于稳定的 UI 宿主命令属于触发瞬间的数据快照。第三确认动作不是“执行闭包”而是一次带前置条件的业务提交。修完后PV-2417连续旋转 30 次没有再出现错删。更重要的是代码不再依赖设备动画有多快即使窗口回调、输入法回调和用户点击以不同顺序到达最终也要经过同一组身份检查。对多形态设备来说这种确定性比再加一个延时更可靠。
返回列表