鸿蒙 PC Markdown 编辑器防误操作设计:未保存标签关闭状态机

发布时间:2026/7/23 13:50:36

鸿蒙 PC Markdown 编辑器防误操作设计:未保存标签关闭状态机
鸿蒙 PC Markdown 编辑器防误操作设计未保存标签关闭状态机桌面编辑器最不能犯的错误是把“用户点击关闭”理解成“可以立即删除文档状态”。一个标签可能从未保存过可能对应磁盘文件可能在保存期间继续输入可能遇到系统选择器取消、外部文件变化或写入失败。关闭动作如果只绑定一个布尔确认框很容易在异步边界上丢失内容。本文基于鸿蒙 PC Markdown 编辑器 OhMarkdown拆解脏标签关闭的取消、放弃、保存三条路径说明为什么保存后关闭必须等待真正持久化成功为什么后台标签要先激活再保存以及如何在关闭最后一个标签时保持工作台可用。完整代码位于 https://gitcode.com/VON-/codex_md_oh本文对应提交3a9146e。先区分关闭意图和关闭提交用户点击标签关闭图标只表达一个意图。应用要先读取目标会话的dirty状态privaterequestCloseDocumentSession(sessionId:string):void{constsessionthis.documentSessions.find((candidate:DocumentSession):booleancandidate.idsessionId);if(!session){return;}if(!session.dirty){this.closeDocumentSession(sessionId);return;}// 脏标签进入确认流程}这个入口名使用requestClose而真正删除会话的方法叫closeDocumentSession命名直接体现两阶段语义。干净标签没有数据风险可以进入关闭提交脏标签必须等待用户选择。查询目标会话而不是读取页面级documentDirty非常重要。用户可能点击一个后台标签的关闭按钮当前活动标签是干净的后台标签却有星号。如果用全局活动状态判断应用会无提示删除后台内容。多标签产品的任何命令都要明确作用对象不能默认目标永远是活动会话。如果 sessionId 已经过期或会话已被其他操作删除函数直接返回。声明式 UI 点击事件与异步操作可能交错业务层不应因为找不到对象而访问空值或误关当前标签。三个按钮代表三种不同事务脏标签确认对话框提供取消、放弃和保存this.getUIContext().getPromptAction().showDialog({title:$r(app.string.unsaved_changes_title),message:$r(app.string.unsaved_changes_message),buttons:[{text:$r(app.string.cancel_action),color:#34404B},{text:$r(app.string.discard_action),color:#B42318},{text:$r(app.string.save_action),color:#087A63}]}).then((result){if(result.index1){this.closeDocumentSession(sessionId);}elseif(result.index2){this.saveAndCloseDocumentSession(sessionId);}}).catch((){this.operationStatusUnable to show confirmation;});索引零是取消因此没有分支状态保持不变。取消不是失败也不需要把标签重新创建因为它从未被删除。索引一是放弃明确跳过保存并提交关闭。索引二是保存进入一条异步链不能直接调用关闭。放弃按钮使用危险色保存使用产品强调色取消保持中性。颜色不是安全机制真正安全性来自行为视觉层只帮助用户在高频操作中区分后果。对话框按钮顺序也要与鸿蒙桌面交互习惯和键盘默认焦点一起验证避免回车意外触发放弃。showDialog返回 Promise显示失败会落到 catch只更新状态栏不会关闭标签。确认 UI 自身异常时保守策略必须是保留内容。所有错误路径都应倾向“不丢数据”而不是为了结束流程强行删除状态。保存按钮不等于保存成功最危险的错误实现是用户点击保存程序发出写入请求然后立刻关闭标签。如果这是未命名文档系统保存选择器还没有返回用户可能取消选择。如果目标文件无权限、磁盘已变化或写入失败标签已经消失编辑缓冲区也可能被清理。OhMarkdown 使用pendingCloseSessionId表示“保存成功后需要关闭的会话”privatependingCloseSessionId:string;privateasyncsaveAndCloseDocumentSession(sessionId:string):Promisevoid{if(sessionId!this.activeDocumentSessionId){awaitthis.activateDocumentSession(sessionId);}if(sessionId!this.activeDocumentSessionId){return;}this.pendingCloseSessionIdsessionId;this.requestEditorCommand(save);}字段不是“关闭计时器”而是保存事务结束后的条件动作。保存流程最终检查会话是否仍活动、是否已经干净只有满足条件才真正关闭。为什么后台标签需要先激活当前文件保存管线读取页面级活动文档字段包括 URI、正文、格式、revision 和持久化基线。直接保存后台会话会把当前页面字段与目标 sessionId 混用。先执行标准标签切换让原生状态和 CodeMirror 状态都指向目标再发出保存命令可以复用经过验证的单活动会话保存路径。切换是异步的所以激活后再次比较 sessionId。ArkWeb 激活可能失败或者文件操作互斥阻止切换此时函数直接返回既不设置 pending也不关闭。重复检查看似保守却封住了“以为切换成功”的竞态。保存内容必须来自编辑器命令快照requestEditorCommand(save)让 Web 编辑器先刷新原生状态和恢复快照再通过 Bridge 发送保存命令。原生onEditorCommand接收正文privateonEditorCommand(command:string,content:string):void{if(commandsave){this.documentContentcontent;this.documentRevision1;this.syncActiveDocumentSession(this.documentContent);this.saveDocument(this.documentRevision);}// 省略其他命令}这样保存使用的是用户点击时的编辑器快照而不是原生层上一次节流同步的旧正文。保存调用同时携带snapshotRevision后续可判断写入期间是否发生新编辑。Bridge 协议在这里承担事务起点编辑器先把当前内容固定为一个版本原生层再写入。若直接由工具栏调用saveDocument按键事件和 Bridge 节流之间可能有时间差最后几个字符不一定进入保存内容。保存期间设置operationInProgress阻止另一个打开、保存或会话切换流程并发修改同一组活动字段。这个互斥让 pendingClose 只对应一笔保存。后续若支持多个后台会话并行保存就需要把保存上下文从页面字段改成显式对象包含 sessionId、URI、正文、格式和 revision而不能仅去掉互斥。系统选择器取消必须保留标签未命名文档没有 URI保存时先打开系统选择器letsaveUrithis.documentUri;constexistingDocumentUrithis.documentUri;if(saveUri.length0){saveUriawaitpickMarkdownSaveUri(context,this.documentName)??;}if(saveUri.length0){this.operationStatusthis.hasUnsavedChanges()?Modified:Ready;return;}用户取消选择后saveUri为空保存函数正常返回documentDirty仍为 true。finally 会看到 pendingClose但因为文档仍脏不执行关闭。标签、正文和星号全部保留。这一行为需要独立测试因为从用户视角他先在关闭确认框点“保存”又在系统选择器点“取消”。第二次取消应当覆盖第一次保存意图而不是被解释为“取消另存为但继续关闭”。多层对话框中最内层未完成的持久化必须阻止外层破坏性动作。混合换行文档还可能出现第二个策略对话框让用户选择 LF、CRLF 或取消constsaveFormatawaitthis.resolveSaveFormat();if(!saveFormat){this.operationStatusthis.hasUnsavedChanges()?Modified:Ready;return;}取消格式选择同样保留 dirty从而阻止关闭。状态机不是只围绕一个确认框而是贯穿保存管线中所有可能终止事务的交互。外部修改冲突也不能关闭已有文件保存前会重新读取磁盘版本与打开时记录的persistedDocumentContent和格式比较constdiskDocumentawaitreadUtf8Document(saveUri);if(existingDocumentUri.length0this.persistedDocumentContent!undefined(diskDocument.content!this.persistedDocumentContent||!this.isSameDocumentFormat(diskDocument.format,this.documentFormat))){thrownewError(The file changed on disk. Reopen it or use Save As to avoid overwriting external changes.);}如果其他应用修改了同一文件当前编辑器不能静默覆盖。异常进入 catch文档继续保持 dirtyfinally 不关闭。用户仍能从标签中复制内容、另存为或重新打开处理冲突。这里再次说明关闭状态不能只依赖“保存函数有没有返回”。保存函数可能因为取消正常返回也可能因为冲突抛错最终判断应读取持久化后的事实即目标会话是否干净。写入失败时先恢复旧文件保存前程序把目标文件旧内容和格式写入应用沙箱备份pendingBackup{version:1,documentUri:saveUri,documentName:diskDocument.name,previousContent:diskDocument.content,hasUtf8Bom:diskDocument.format.hasUtf8Bom,lineEnding:diskDocument.format.lineEnding,updatedAt:Date.now()};awaitsavePendingSaveBackup(context.filesDir,pendingBackup);真正写入失败后catch 尝试把旧版本写回恢复成功则清理备份恢复也失败则保留沙箱记录供下次启动处理。无论哪种情况当前编辑缓冲区都不应被删除。}catch(error){constfailureMessageerrorinstanceofError?error.message:String(error);if(pendingBackup){try{awaitwriteUtf8Document(pendingBackup.documentUri,pendingBackup.previousContent,{hasUtf8Bom:pendingBackup.hasUtf8Bom,lineEnding:this.parseLineEnding(pendingBackup.lineEnding)});awaitclearPendingSaveBackup(context.filesDir);this.operationStatusSave failed; previous file restored:${failureMessage};}catch(_){this.operationStatusSave failed; backup retained:${failureMessage};}}}关闭安全与文件安全在这里汇合只要写入没有建立新的干净基线pendingClose 就不能提交。即使旧磁盘文件已恢复用户的新编辑仍只存在缓冲区关闭会造成损失。revision 防止保存期间的新输入被误判为已保存保存开始时记录snapshotRevision。写入成功后只有当前 revision 仍等于快照版本才能清除 dirtyif(this.documentRevisionsnapshotRevision){this.documentDirtyfalse;this.operationStatusbackupCleared?Saved:Saved; backup cleanup pending;this.clearRecoveryDraft();}else{this.operationStatusModified;}假设写入耗时较长用户在保存过程中又输入一行。磁盘只包含保存开始时的快照新一行尚未持久化。若无条件设为干净标签星号消失finally 随后关闭最后一行就会丢失。revision 比较让它继续保持ModifiedpendingClose 因此不执行。这是一种乐观版本控制写入操作可以异步完成但提交“已保存”状态前确认数据没有变化。它比简单禁用编辑器更符合桌面体验用户不必等待磁盘代价是需要正确维护每次编辑的 revision。markEditorSaved()会把 CodeMirror 内的保存基线更新到实际快照原生层随后同步活动会话。保存基线、revision 和 dirty 三者必须一起迁移不能只改标签星号。finally 中的关闭条件保存流程无论成功、取消还是失败都会进入 finally}finally{constcloseSessionIdthis.pendingCloseSessionId;this.pendingCloseSessionId;if(closeSessionId.length0closeSessionIdthis.activeDocumentSessionId!this.documentDirty){awaitthis.closeDocumentSession(closeSessionId);}this.operationInProgressfalse;}先把 pending 字段复制到局部变量并清空保证这笔意图只消费一次。随后检查三个条件确实有待关闭会话它仍是当前活动会话文档已经干净。任何条件不满足都保留标签。检查活动身份防止异步期间关闭错标签。当前互斥已大幅减少切换但身份检查仍是必要防线。检查 dirty 则统一覆盖选择器取消、格式取消、外部冲突、写入失败、恢复失败和保存期间继续输入。最后才把operationInProgress设回 false。closeDocumentSession内部还要切换后继会话并操作 ArkWeb如果提前释放互斥用户可能在关闭尚未完成时发出新命令。当前顺序让整个“保存并关闭”成为一个完整事务。放弃关闭与恢复记录用户选择放弃时程序不写磁盘直接删除目标会话。这是用户明确授权的数据丢弃路径。关闭活动会话后还会清理当前恢复草稿避免下次启动又弹出已被用户放弃的内容。但多标签崩溃恢复目前主要保证活动标签后台标签的恢复记录尚未完整持久化。因此关闭后台脏标签时放弃语义只作用于内存会话未来扩展为多会话恢复集合后必须按 sessionId 或 URI 删除对应记录而不是清空全部草稿。取消关闭则绝不能清理恢复记录。保存成功后可以清理因为磁盘建立了新基线放弃后可以清理因为用户明确不要显示对话框失败、系统选择器取消、写入失败都必须保留。最后一个标签关闭后仍保留工作台关闭提交从会话数组删除目标。如果删除后数组为空程序创建一个新的干净未命名会话if(remainingSessions.length0){remainingSessions[createUntitledSession(this.createDocumentSessionId())];}这样 Web 编辑器始终有活动 sessionId工具栏的打开、新建、保存和快捷键不需要处理“零会话”特例。用户看到的是一个空白工作台而不是编辑区域突然消失。关闭中间标签时后继选择原索引对应的右侧邻居关闭末尾标签时选择新数组最后一项constnextIndexMath.min(sessionIndex,remainingSessions.length-1);constnextSessionremainingSessions[nextIndex];this.applyDocumentSession(nextSession);awaitthis.activateEditorSession(nextSession);this.closeEditorSession(sessionId);先激活后继再清理旧 Web 会话避免编辑器出现没有状态的中间帧。关闭非活动标签则无需切换只删除数组项和对应快照。鸿蒙 PC 模拟器中的脏标签下图来自 MateBook Pro 2in1 模拟器。标签栏同时存在两个未命名文档活动标签正文为Session-B标签状态与底部Modified一致。这样的独立脏状态是关闭确认能够正确作用到目标标签的前提。模拟器验证完成了取消和放弃分支点击脏标签关闭图标选择取消后标签和正文保留再次关闭并选择放弃只删除目标标签邻近标签恢复且内容不串页。保存分支还要覆盖已有文件直接保存、未命名文件选择 URI、选择器取消、写入失败和保存期间继续输入。测试报告不能把“调用保存方法”写成保存分支通过。真正通过标准是目标 URI 写入成功、dirty 清除后标签关闭任何取消和失败都必须保留。状态机的验收围绕数据结果而不是按钮路径。应用退出与窗口关闭仍需单独设计标签关闭状态机解决单个会话不自动等于应用退出保护。用户点击窗口关闭时可能有多个脏标签需要决定逐个询问、汇总列表、全部保存或取消退出。若简单调用当前标签的关闭对话框后台脏会话仍可能丢失。完整退出流程可以收集所有 dirty 会话显示可勾选列表再按顺序保存。未命名文档会依次弹出系统选择器任何取消都应中止退出。多个保存的错误汇总、部分成功后的状态、恢复记录清理也要定义。应用被系统强杀则不能弹框只能依赖周期恢复快照。因此“关闭标签”“关闭窗口”“进程终止”是三种不同生命周期。它们可以复用会话 dirty 和保存事务但不能共享一个简单确认函数后假设问题已经解决。结语未保存标签关闭是一台小型事务状态机点击关闭只建立意图干净标签直接提交脏标签等待取消、放弃或保存保存目标若在后台先完成会话切换系统选择器、格式选择、外部冲突和写入异常都可以中止revision 确认保存期间没有新输入最终只在同一会话已经干净时关闭。这套设计的核心标准很简单所有不确定路径都保留用户内容只有持久化成功或用户明确放弃才能删除会话。鸿蒙 PC 编辑器要进入长期真实写作场景防误操作不能依赖一句“是否保存”而要贯穿文件系统、Bridge、编辑器状态和多标签生命周期的完整调用链。

相关新闻

鸿蒙 PC Markdown 编辑器外部修改检测:从文件指纹到冲突决策

鸿蒙 PC Markdown 编辑器外部修改检测:从文件指纹到冲突决策

2026/7/23 13:50:36

鸿蒙 PC Markdown 编辑器外部修改检测:从文件指纹到冲突决策 桌面 Markdown 文件很少只被一个程序触碰。用户可能在终端执行格式化脚本、在 Git 客户端切换分支、让生成器更新文档,或者同时打开另一款编辑器。应用如果假设“打开之后磁盘永远不变”&…

35岁带团队,被95后用 Copilot 上了一课

35岁带团队,被95后用 Copilot 上了一课

2026/7/23 13:40:35

我带了八年团队,上周被 95 后当众上了一课 评审会上,我让小组报进度。干了五年的老部下慢吞吞:"这个报表模块,估三天。"旁边入职半年的 97 年小哥直接把笔记本转过来:"哥,我刚用 Cursor 二十…

工业质检中LLM的SFT与RAG融合应用实践

工业质检中LLM的SFT与RAG融合应用实践

2026/7/23 13:40:35

1. 项目背景与核心挑战在工业质检领域,质量事件的处理效率直接影响生产线的整体效能。传统基于规则的质量检测系统在面对复杂缺陷类型时往往表现不佳,而大语言模型(LLM)的出现为解决这一问题提供了新的技术路径。这个项目聚焦于如何通过监督微调(SFT)策略…

BQ27505-J4数据闪存访问与阻抗跟踪算法调优实战指南

BQ27505-J4数据闪存访问与阻抗跟踪算法调优实战指南

2026/7/23 14:30:37

1. 项目概述与核心价值在移动设备和便携式电子产品的开发中,电池管理系统(BMS)的精度直接决定了用户体验的底线。用户最怕的莫过于电量显示“跳水”——明明还有20%,转眼就自动关机。这背后,是电池这个复杂的电化学系统…

三种常用的数据存储技术

三种常用的数据存储技术

2026/7/23 14:30:37

数据存储技术 当今最主流、最常用的数据存数技术有MySQL、SQLite、Redis,他们三者各有不同的长处,用途也不同,可以互相配合使用,对比如下MySQL(存中心)SQLite(存本地)Redis&#xff…

职场工作服套装生产厂家如何选择?

职场工作服套装生产厂家如何选择?

2026/7/23 14:30:37

选择合适的职场工作服套装生产厂家对于企业来说至关重要,以下是一些选择要点:一、生产能力与规模厂房与设备 考察厂家是否有现代化的标准生产厂房,像深圳市凯思顿服饰有限公司,拥有5000平方米的厂房,厂区布局规整&…

TPS6131x I2C寄存器配置与PCB布局实战:驱动手机闪光灯的精密电源管理

TPS6131x I2C寄存器配置与PCB布局实战:驱动手机闪光灯的精密电源管理

2026/7/23 14:30:37

1. 项目概述与核心价值在手机、平板这类便携设备里,给摄像头闪光灯或者手电筒供电,看起来是个简单的活儿,不就是让LED亮起来嘛。但真干过这行的工程师都知道,这里面的水可深了。你得考虑电池电压会波动,LED的亮度要精准…

AI Agent在智能供应链需求预测中的应用与优化

AI Agent在智能供应链需求预测中的应用与优化

2026/7/23 14:30:37

1. AI Agent如何重塑供应链需求预测的底层逻辑在传统供应链管理中,需求预测往往依赖历史销售数据的统计分析,采用移动平均法、指数平滑法等经典算法。这种模式存在两个致命缺陷:一是对突发事件的响应滞后(比如2020年疫情初期&…

什么是商户进件?

什么是商户进件?

2026/7/23 14:20:37

商户进件,简单来说就是商家向支付平台上交经营相关证件,申请开通收款渠道。需要准备营业执照、法人身份证、收款银行卡以及门店经营相关证明。操作流程:提交全部资料后等待平台风控审核,审核通过就能正常收款。一方面这是商家开通…

微服务进阶:服务网格与Istio

微服务进阶:服务网格与Istio

2026/7/23 3:40:08

541|微服务进阶:服务网格与Istio 上篇文章我们聊了微服务的基本概念和拆分方法。 但微服务多了,问题也多了: 服务之间怎么通信? 怎么监控每个服务的调用链路? 熔断、限流、重试怎么做? 安全认证怎么统一? 以前这些都靠SDK库(比如Hystrix、Feign),每个服务都要集成…

零售超级终端全域协同:ShareKit 碰一碰商品流转业务落地案例

零售超级终端全域协同:ShareKit 碰一碰商品流转业务落地案例

2026/7/23 4:40:05

一、零售门店全域协同业务背景与行业痛点 1.1 门店超级终端设备矩阵(连锁便利店/商超标准配置) 自助收银Kiosk一体机:顾客结算、自助核销优惠券、商品素材预览;运营折叠平板:店长后台商品上新、图片录入、活动配置、…

噗叽短视频界面分析

噗叽短视频界面分析

2026/7/23 1:54:13

1 和小红书类似,可以采用类似判断方法------------其实他比小红书好判断,因为他没有图片,控件位置几乎是固定的,都不用判断------------2 因为他没有点赞按钮------------而且几乎所有控件位置都是完全一样的,所以我就…

企业级AI搜索落地选型实战手册(含LLM+RAG+Hybrid架构对比矩阵与ROI测算模板)

企业级AI搜索落地选型实战手册(含LLM+RAG+Hybrid架构对比矩阵与ROI测算模板)

2026/7/23 0:09:56

更多请点击: https://kaifayun.com 第一章:企业级AI搜索落地选型实战手册(含LLMRAGHybrid架构对比矩阵与ROI测算模板) 企业级AI搜索系统落地成败,核心在于技术选型与业务价值的精准对齐。盲目堆砌大模型能力或过度依赖…

TM4C129LNCZAD外设实战:LCD、比较器与PWM寄存器配置详解

TM4C129LNCZAD外设实战:LCD、比较器与PWM寄存器配置详解

2026/7/23 0:09:56

1. 项目概述与核心价值在嵌入式系统开发,尤其是基于ARM Cortex-M内核的微控制器项目中,深入理解并熟练配置芯片的片上外设,是从“点亮LED”迈向“实现复杂系统功能”的关键一步。Tiva™ TM4C129LNCZAD作为TI公司Cortex-M4F家族中的高性能成员…

AtomCode `fmt_dur` 争议溯源:两个函数、三段演进、四个事实

AtomCode `fmt_dur` 争议溯源:两个函数、三段演进、四个事实

2026/7/23 0:09:56

一、快速声明与争议背景本文是对 AtomCode 终端 spinner 时长显示 fmt_dur 相关说法的事实性核验。2026 年 7 月 CSDN 上出现两篇互相矛盾的博文,近期又有 AI 在对话中输出格式描述 XhYm / YmZs / Zs。本文基于 AtomCode 仓库 main4677ddfa 及全分支 Git 历史给出可…