鸿蒙 PC Markdown 编辑器 ArkUI 界面分层从超大工作台到可维护组件边界鸿蒙 PC 上的 Markdown 编辑器并不是把一个手机页面放大。桌面窗口里同时存在活动栏、文件树、全文搜索、大纲、版本历史、设置、十二文档标签、编辑器模式、状态栏、拖放、右键菜单、自由窗口和可调侧栏。如果所有 ArkUI 展示与系统编排都持续写进同一个组件功能表面上仍能运行但修改成本会快速上升搜索面板的一处复选框布局可能需要穿过文件保存、恢复、分享和多窗口代码才能定位标签右键菜单的局部状态也会与文档事实状态混在一起。这篇文章完整说明一套适合鸿蒙 PC Markdown 编辑器的渐进式界面分层方案。它不引入新的状态管理框架不把现有工程推倒重写也不改变 Markdown 文档、文件服务和 ArkWeb Bridge 的行为。核心目标是让代码所有权与用户看见的桌面区域一致并用编译、结构门禁、自动化和模拟器交互证明拆分没有把“能运行”换成“看起来更整齐”。项目仓库地址https://gitcode.com/VON-/codex_md_oh本次实现提交9f3eab0问题不只是文件行数重构前工作台入口WorkspaceShell.ets达到 5011 行。单看行数并不能证明设计错误有些复杂状态机确实需要较长代码真正的问题是同一文件包含了不同变化原因窗口级文档会话、未保存状态和操作状态Core File Kit 打开、读取、保存与指纹检测崩溃恢复、版本历史和窗口会话恢复ArkWeb JavaScriptProxy 白名单 Bridge活动栏、标签栏、搜索、文件树、大纲、历史和设置展示侧栏拖动悬停、标签右键弹层等短生命周期交互状态导出 HTML、打印 PDF、PNG 与系统分享命令多窗口、外部修改冲突和关闭确认编排。这些代码的运行时关系并不相同。文档正文、活动标签和侧栏宽度是需要跨重绘保持、部分需要持久化的窗口事实鼠标是否正悬停在分隔线上只属于一个可视控件当前几百毫秒的交互。把两者都提升到同一个容器看似统一实际会模糊状态所有权。所以本次目标不是机械地把 5011 行切成若干个 300 行文件而是回答三个问题谁持有事实状态谁只负责展示谁连接外部边界。分层后的目录语义最终界面目录形成五个清晰区域shared/ui/ ├── WorkspaceShell.ets ├── bridge/ │ └── EditorBridge.ets ├── model/ │ └── WorkspaceViewModels.ets ├── components/ │ ├── EditorSurface.ets │ ├── ExternalConflictBanner.ets │ ├── WorkspaceActivityRail.ets │ ├── WorkspaceControls.ets │ ├── WorkspaceDocumentBar.ets │ ├── WorkspaceSidebarResizeHandle.ets │ └── WorkspaceStatusBar.ets └── panels/ ├── WorkspaceFilesPanel.ets ├── WorkspaceHistoryPanel.ets ├── WorkspaceOutlinePanel.ets ├── WorkspaceSearchPanel.ets └── WorkspaceSettingsPanel.etsWorkspaceShell仍然重要但职责收缩为容器和应用协调器。它持有每个窗口的文档会话、当前面板、视图模式、操作状态和侧栏宽度调用services/完成文件、搜索、版本历史与设置能力再把可渲染数据和动作回调交给子组件。panels对应桌面活动栏可以切换的完整区域。文件、搜索、大纲、历史、设置都具有独立标题、控件组合、列表和空状态因此它们是稳定的业务面板边界。components对应可以独立理解的工作台区域。文档标签加顶部工具栏是一个组件活动栏、状态栏、冲突横幅、侧栏分隔线各是一个组件。按钮、分段按钮和搜索选项等重复视觉原语集中到WorkspaceControls.ets但没有为每个按钮创建单独文件。bridge明确表示跨 ArkUI 与 ArkWeb 的边界适配器。EditorBridge不是视图模型它只把 JavaScriptProxy 白名单调用转发给容器因此从原来的混合位置移入 Bridge 层。model只保存 Shell 与面板共同使用的 UI 类型。例如搜索面板模式需要同时被父容器的命令逻辑和子面板的按钮选择使用exportenumSearchPanelMode{DOCUMENTdocument,WORKSPACEworkspace,QUICK_OPENquick-open}它不包含文件 I/O也不试图复制业务 service。这个约束很关键否则“分层”很容易退化为把相同业务规则在 UI 目录再写一次。容器持有事实面板接收数据与意图搜索面板是最能体现边界的一部分。它包含当前文档查找、工作区搜索、快速打开、大小写、整词、正则、替换和结果列表。直接让面板调用搜索服务会减少几行回调但会带来两个问题面板开始拥有任务取消与代际状态多窗口下又要判断当前会话属于哪个 Shell。因此面板只声明输入与动作Componentexportstruct WorkspaceSearchPanel{PropsidebarWidth:number;Propexpanded:boolean;Propmode:SearchPanelMode;PropmatchCount:number;PropactiveQuery:string;PropreplaceQuery:string;Propresults:ArrayWorkspaceSearchResult;onModeChange:(mode:SearchPanelMode)void(){};onQueryChange:(value:string)void(){};onSubmit:()void(){};onCancel:()void(){};onReplaceCurrent:()void(){};onReplaceAll:()void(){};onOpenResult:(result:WorkspaceSearchResult)void(){};}Shell 的装配代码则显式表达每个动作最终去哪WorkspaceSearchPanel({sidebarWidth:this.sidebarWidth,expanded:this.usesExpandedToolbar(),mode:this.searchPanelMode,matchCount:this.searchMatchCount,activeQuery:this.getActiveSearchQuery(),replaceQuery:this.replaceQuery,results:this.workspaceSearchResults,onModeChange:(mode:SearchPanelMode)this.setSearchPanelMode(mode),onQueryChange:(value:string)this.updateActiveSearchQuery(value),onSubmit:()this.submitSearchInput(),onCancel:()this.cancelWorkspaceSearch(),onReplaceCurrent:()this.replaceCurrentMatch(),onReplaceAll:()this.replaceAllMatches(),onOpenResult:(result:WorkspaceSearchResult)this.openWorkspaceSearchResult(result)})这种写法的代价是装配参数较多但它比隐式共享状态更适合 PC 编辑器。读代码时无需搜索事件总线也不会出现另一个窗口的搜索结果误写当前窗口。类型变化会在 ArkTS 编译阶段直接暴露而不是运行到某个冷门面板才发现。局部交互状态应该留在组件内部不是所有状态都必须由 Shell 管理。标签右键菜单的可见性、被点击标签索引和会话 ID只服务于WorkspaceDocumentBar。侧栏拖动时的悬停、激活和起始宽度只服务于WorkspaceSidebarResizeHandle。这些状态既不需要写入窗口会话也不应该触发恢复记录。侧栏组件把短生命周期状态留在内部把最终宽度仍交给父容器Componentexportstruct WorkspaceSidebarResizeHandle{PropsidebarWidth:number;ProphandleWidth:number;Stateprivatehovered:booleanfalse;Stateprivateactive:booleanfalse;privatestartWidth:number0;onWidthChange:(width:number)void(){};onCommit:()void(){};onKeyAction:(event:KeyEvent)boolean()false;}拖动更新只上报候选宽度Shell 继续执行 PC 窗口预算限制WorkspaceSidebarResizeHandle({sidebarWidth:this.sidebarWidth,handleWidth:SIDEBAR_RESIZE_HANDLE_WIDTH,onWidthChange:(width:number){this.sidebarWidththis.clampSidebarWidth(width);},onCommit:()this.persistSidebarWidth(),onKeyAction:(event:KeyEvent):booleanthis.handleSidebarResizeKey(event)})这样既避免了组件越权写 Preferences也保留了拖动的连续反馈。鼠标离开或组件卸载时组件自己恢复系统指针方向键微调和持久化继续复用容器的既有规则。文档标签栏为何是一个复合组件标签栏不是一排普通文本。它需要显示活动状态、未保存标记、关闭按钮、右键菜单、最多十二标签的水平滚动同一条顶部区域还承载打开、保存、源码、即时、分栏、预览、同步滚动、新窗口和侧栏命令并接受原生文件拖放。如果只把单个标签抽成组件而工具栏仍留在 Shell标签右键弹层、水平滚动和拖放边界仍然散落。最终选择WorkspaceDocumentBar作为复合区域是因为这些控件共享同一条稳定桌面空间和相同的响应式策略。模式按钮仍复用通用控件并保留大文档保护WorkspaceModeButton({label:$r(app.string.preview_mode),mode:preview,selectedMode:this.selectedMode,expanded:this.expanded,isEnabled:!this.largeDocumentMode,action:this.onModeChange})这里没有把“为什么大文档禁用预览”的规则复制到组件。组件只收到largeDocumentMode并据此控制按钮可用性真正的大文档阈值和编辑器配置仍由既有性能服务与 Shell 负责。Bridge 是边界适配器不是第二份状态ArkWeb 编辑器通过白名单对象向 ArkUI 上报 ready、字数、脏状态、快照、图片导入、图片读取和命令。拆分时最危险的做法是让每个面板各自暴露一个 Bridge或在 Bridge 中保存一份当前文档。本次只移动EditorBridge的文件所有权不改变协议exportclassEditorBridge{privatereadonlysnapshotHandler:(content:string,revision:number)void;privatereadonlycommandHandler:(command:string,content:string)void;onSnapshot(content:string,revision:number):void{this.snapshotHandler(content,revision);}onCommand(command:string,content:string):void{this.commandHandler(command,content);}}它仍然只是函数转发器。CodeMirror 的 EditorState 是编辑期正文事实来源ArkUI 的 DocumentSession 是窗口会话事实Bridge 不缓存正文也不提供任意方法调用。这使目录更清楚但没有增加新的同步问题。防止半年后又长回一个文件一次重构并不能保证长期边界。新需求赶时间时最容易发生的事情是“先把面板写回 Shell后面再拆”。所以工程增加了结构门禁scripts/verify-ui-layering.sh。门禁做三类检查shell_lines$(wc-l$SHELL_FILE|tr-d )if[$shell_lines-gt4000];thenechoUI 分层检查失败WorkspaceShell.ets 超过 4000 行上限。2exit1ficomponent_count$(awk/^Component$/ { count 1 } END { print count 0 }$SHELL_FILE)if[$component_count-ne1];thenechoWorkspaceShell.ets 只应声明一个容器组件。2exit1fi它还检查 bridge、model、components 和 panels 的关键文件是否存在并接入统一的verify-local.sh。4000 行不是代码质量分数而是一个回退报警器当前 Shell 为 3551 行预留了合理装配空间但不允许整块展示 UI 无声回流。如果未来新增的确是复杂应用协调逻辑应该先复核 ADR而不是为了绿灯随意调高数字。真实鸿蒙 PC 模拟器验证下面的图片来自 HarmonyOS 6.1.1 API 24 MateBook Pro 2in1 模拟器使用最终 Debug HAP 运行后由snapshot_display直接采集。截图中搜索面板已经作为独立WorkspaceSearchPanel渲染侧栏由拖动条从较窄宽度扩大后三个搜索选项恢复为单行标签、工具栏、预览和状态栏同时保持完整。设备验证不是只看截图。实际执行路径包括启动最终 Debug HAP确认 ArkWeb Bridge 完成加载并显示已有 Markdown 预览。从文件面板切换到大纲面板确认标题数量与跳转行正常显示。切换到搜索面板确认当前文档、工作区、快速打开三个模式和查找替换控件完整。拖动侧栏分隔线约 176 像素确认父容器收到宽度变化、编辑区同步缩放、搜索选项响应式切换。运行模拟器 ohosTest确认文件、恢复、窗口会话、版本历史、搜索、链接和设置服务没有因 UI 移动回退。测试结果与第一次失败的意义最终验证结果如下UI 结构门禁通过WorkspaceShell.ets3551 行只包含一个Component容器声明CommonMark 0.31.2 conforming 配置652/652GFM 冻结扩展语料 30 例通过Web Playwright66/66Debug HAP 构建通过ArkTSUnitTestBuild通过ohosTest HAP 构建通过MateBook Pro 2in1 模拟器 ohosTest16/16Failure 0、Error 0总耗时 2267 ms模拟器搜索面板切换和侧栏拖动通过。验证过程中出现过一次真实失败EditorBridge从model移到bridge后Shell 的导入已经修改但EditorSurface.ets仍指向旧路径。Web 的 66 项测试全部通过随后 ArkTS 编译报出无法解析../model/EditorBridge。这正好说明混合工程不能只跑 Web 测试。修正组件导入后重新执行完整统一验证Web、Debug HAP 和 UnitTestBuild 才全部通过。这个过程没有被从报告中删除因为工程证据的价值不只是最后一行绿色输出。它证明结构调整需要覆盖依赖图的不同编译单元也证明统一脚本应该把 Web、结构检查和 ArkTS 构建串在一起。为什么没有继续拆成更多 Controller分层后 Shell 仍有 3551 行主要是保存、恢复、外部冲突、搜索任务、导出、分享、多窗口和窗口关闭编排。继续拆当然可能让数字更小但这会跨越多个已经稳定的系统能力链路。本次采用 Level 2、D2只拆当前用户明确指出的 UI 所有权问题保持 service 契约和窗口状态模型不变。没有引入全局 store、事件总线、依赖注入或新的 Controller 框架。后续只有在某条业务编排出现第二个真实调用方、难以单元测试或再次逼近结构门禁时才按纵切提取独立控制器。这种克制对鸿蒙 PC 文档软件尤其重要。文件写入、崩溃恢复和多窗口冲突不是适合为了目录美观而大范围搬动的代码。展示层先变清楚系统能力继续稳定是更可验证的顺序。可以复用的判断方法为其他 ArkUI 桌面应用拆分超大页面时可以用四个问题判断代码应该去哪这个状态是否需要跨重绘、重启或窗口恢复需要时由容器或 service 持有只描述悬停和弹层时由组件持有。这个区域是否对应用户能够命名的完整桌面区域如果是它适合作为 panel 或 composite component。这个代码是否接触文件、网络、系统窗口或持久化如果是它不应藏在纯展示面板内部。这个抽象是否减少当前真实的心智负担如果只是把十行代码搬到新文件且没有稳定边界就不值得创建。最终可维护性并不来自文件数量而来自稳定的依赖方向服务提供能力容器组织状态与命令面板和组件展示数据并上报意图Bridge 只连接受限边界。对于优先适配鸿蒙 PC 的 Markdown 编辑器这种层次能够让后续安全、隐私、发布和更多桌面功能继续推进同时避免工作台再次退化成一个任何修改都牵动全局的超大文件。
鸿蒙 PC Markdown 编辑器 ArkUI 界面分层:从超大工作台到可维护组件边界
鸿蒙 PC Markdown 编辑器 ArkUI 界面分层从超大工作台到可维护组件边界鸿蒙 PC 上的 Markdown 编辑器并不是把一个手机页面放大。桌面窗口里同时存在活动栏、文件树、全文搜索、大纲、版本历史、设置、十二文档标签、编辑器模式、状态栏、拖放、右键菜单、自由窗口和可调侧栏。如果所有 ArkUI 展示与系统编排都持续写进同一个组件功能表面上仍能运行但修改成本会快速上升搜索面板的一处复选框布局可能需要穿过文件保存、恢复、分享和多窗口代码才能定位标签右键菜单的局部状态也会与文档事实状态混在一起。这篇文章完整说明一套适合鸿蒙 PC Markdown 编辑器的渐进式界面分层方案。它不引入新的状态管理框架不把现有工程推倒重写也不改变 Markdown 文档、文件服务和 ArkWeb Bridge 的行为。核心目标是让代码所有权与用户看见的桌面区域一致并用编译、结构门禁、自动化和模拟器交互证明拆分没有把“能运行”换成“看起来更整齐”。项目仓库地址https://gitcode.com/VON-/codex_md_oh本次实现提交9f3eab0问题不只是文件行数重构前工作台入口WorkspaceShell.ets达到 5011 行。单看行数并不能证明设计错误有些复杂状态机确实需要较长代码真正的问题是同一文件包含了不同变化原因窗口级文档会话、未保存状态和操作状态Core File Kit 打开、读取、保存与指纹检测崩溃恢复、版本历史和窗口会话恢复ArkWeb JavaScriptProxy 白名单 Bridge活动栏、标签栏、搜索、文件树、大纲、历史和设置展示侧栏拖动悬停、标签右键弹层等短生命周期交互状态导出 HTML、打印 PDF、PNG 与系统分享命令多窗口、外部修改冲突和关闭确认编排。这些代码的运行时关系并不相同。文档正文、活动标签和侧栏宽度是需要跨重绘保持、部分需要持久化的窗口事实鼠标是否正悬停在分隔线上只属于一个可视控件当前几百毫秒的交互。把两者都提升到同一个容器看似统一实际会模糊状态所有权。所以本次目标不是机械地把 5011 行切成若干个 300 行文件而是回答三个问题谁持有事实状态谁只负责展示谁连接外部边界。分层后的目录语义最终界面目录形成五个清晰区域shared/ui/ ├── WorkspaceShell.ets ├── bridge/ │ └── EditorBridge.ets ├── model/ │ └── WorkspaceViewModels.ets ├── components/ │ ├── EditorSurface.ets │ ├── ExternalConflictBanner.ets │ ├── WorkspaceActivityRail.ets │ ├── WorkspaceControls.ets │ ├── WorkspaceDocumentBar.ets │ ├── WorkspaceSidebarResizeHandle.ets │ └── WorkspaceStatusBar.ets └── panels/ ├── WorkspaceFilesPanel.ets ├── WorkspaceHistoryPanel.ets ├── WorkspaceOutlinePanel.ets ├── WorkspaceSearchPanel.ets └── WorkspaceSettingsPanel.etsWorkspaceShell仍然重要但职责收缩为容器和应用协调器。它持有每个窗口的文档会话、当前面板、视图模式、操作状态和侧栏宽度调用services/完成文件、搜索、版本历史与设置能力再把可渲染数据和动作回调交给子组件。panels对应桌面活动栏可以切换的完整区域。文件、搜索、大纲、历史、设置都具有独立标题、控件组合、列表和空状态因此它们是稳定的业务面板边界。components对应可以独立理解的工作台区域。文档标签加顶部工具栏是一个组件活动栏、状态栏、冲突横幅、侧栏分隔线各是一个组件。按钮、分段按钮和搜索选项等重复视觉原语集中到WorkspaceControls.ets但没有为每个按钮创建单独文件。bridge明确表示跨 ArkUI 与 ArkWeb 的边界适配器。EditorBridge不是视图模型它只把 JavaScriptProxy 白名单调用转发给容器因此从原来的混合位置移入 Bridge 层。model只保存 Shell 与面板共同使用的 UI 类型。例如搜索面板模式需要同时被父容器的命令逻辑和子面板的按钮选择使用exportenumSearchPanelMode{DOCUMENTdocument,WORKSPACEworkspace,QUICK_OPENquick-open}它不包含文件 I/O也不试图复制业务 service。这个约束很关键否则“分层”很容易退化为把相同业务规则在 UI 目录再写一次。容器持有事实面板接收数据与意图搜索面板是最能体现边界的一部分。它包含当前文档查找、工作区搜索、快速打开、大小写、整词、正则、替换和结果列表。直接让面板调用搜索服务会减少几行回调但会带来两个问题面板开始拥有任务取消与代际状态多窗口下又要判断当前会话属于哪个 Shell。因此面板只声明输入与动作Componentexportstruct WorkspaceSearchPanel{PropsidebarWidth:number;Propexpanded:boolean;Propmode:SearchPanelMode;PropmatchCount:number;PropactiveQuery:string;PropreplaceQuery:string;Propresults:ArrayWorkspaceSearchResult;onModeChange:(mode:SearchPanelMode)void(){};onQueryChange:(value:string)void(){};onSubmit:()void(){};onCancel:()void(){};onReplaceCurrent:()void(){};onReplaceAll:()void(){};onOpenResult:(result:WorkspaceSearchResult)void(){};}Shell 的装配代码则显式表达每个动作最终去哪WorkspaceSearchPanel({sidebarWidth:this.sidebarWidth,expanded:this.usesExpandedToolbar(),mode:this.searchPanelMode,matchCount:this.searchMatchCount,activeQuery:this.getActiveSearchQuery(),replaceQuery:this.replaceQuery,results:this.workspaceSearchResults,onModeChange:(mode:SearchPanelMode)this.setSearchPanelMode(mode),onQueryChange:(value:string)this.updateActiveSearchQuery(value),onSubmit:()this.submitSearchInput(),onCancel:()this.cancelWorkspaceSearch(),onReplaceCurrent:()this.replaceCurrentMatch(),onReplaceAll:()this.replaceAllMatches(),onOpenResult:(result:WorkspaceSearchResult)this.openWorkspaceSearchResult(result)})这种写法的代价是装配参数较多但它比隐式共享状态更适合 PC 编辑器。读代码时无需搜索事件总线也不会出现另一个窗口的搜索结果误写当前窗口。类型变化会在 ArkTS 编译阶段直接暴露而不是运行到某个冷门面板才发现。局部交互状态应该留在组件内部不是所有状态都必须由 Shell 管理。标签右键菜单的可见性、被点击标签索引和会话 ID只服务于WorkspaceDocumentBar。侧栏拖动时的悬停、激活和起始宽度只服务于WorkspaceSidebarResizeHandle。这些状态既不需要写入窗口会话也不应该触发恢复记录。侧栏组件把短生命周期状态留在内部把最终宽度仍交给父容器Componentexportstruct WorkspaceSidebarResizeHandle{PropsidebarWidth:number;ProphandleWidth:number;Stateprivatehovered:booleanfalse;Stateprivateactive:booleanfalse;privatestartWidth:number0;onWidthChange:(width:number)void(){};onCommit:()void(){};onKeyAction:(event:KeyEvent)boolean()false;}拖动更新只上报候选宽度Shell 继续执行 PC 窗口预算限制WorkspaceSidebarResizeHandle({sidebarWidth:this.sidebarWidth,handleWidth:SIDEBAR_RESIZE_HANDLE_WIDTH,onWidthChange:(width:number){this.sidebarWidththis.clampSidebarWidth(width);},onCommit:()this.persistSidebarWidth(),onKeyAction:(event:KeyEvent):booleanthis.handleSidebarResizeKey(event)})这样既避免了组件越权写 Preferences也保留了拖动的连续反馈。鼠标离开或组件卸载时组件自己恢复系统指针方向键微调和持久化继续复用容器的既有规则。文档标签栏为何是一个复合组件标签栏不是一排普通文本。它需要显示活动状态、未保存标记、关闭按钮、右键菜单、最多十二标签的水平滚动同一条顶部区域还承载打开、保存、源码、即时、分栏、预览、同步滚动、新窗口和侧栏命令并接受原生文件拖放。如果只把单个标签抽成组件而工具栏仍留在 Shell标签右键弹层、水平滚动和拖放边界仍然散落。最终选择WorkspaceDocumentBar作为复合区域是因为这些控件共享同一条稳定桌面空间和相同的响应式策略。模式按钮仍复用通用控件并保留大文档保护WorkspaceModeButton({label:$r(app.string.preview_mode),mode:preview,selectedMode:this.selectedMode,expanded:this.expanded,isEnabled:!this.largeDocumentMode,action:this.onModeChange})这里没有把“为什么大文档禁用预览”的规则复制到组件。组件只收到largeDocumentMode并据此控制按钮可用性真正的大文档阈值和编辑器配置仍由既有性能服务与 Shell 负责。Bridge 是边界适配器不是第二份状态ArkWeb 编辑器通过白名单对象向 ArkUI 上报 ready、字数、脏状态、快照、图片导入、图片读取和命令。拆分时最危险的做法是让每个面板各自暴露一个 Bridge或在 Bridge 中保存一份当前文档。本次只移动EditorBridge的文件所有权不改变协议exportclassEditorBridge{privatereadonlysnapshotHandler:(content:string,revision:number)void;privatereadonlycommandHandler:(command:string,content:string)void;onSnapshot(content:string,revision:number):void{this.snapshotHandler(content,revision);}onCommand(command:string,content:string):void{this.commandHandler(command,content);}}它仍然只是函数转发器。CodeMirror 的 EditorState 是编辑期正文事实来源ArkUI 的 DocumentSession 是窗口会话事实Bridge 不缓存正文也不提供任意方法调用。这使目录更清楚但没有增加新的同步问题。防止半年后又长回一个文件一次重构并不能保证长期边界。新需求赶时间时最容易发生的事情是“先把面板写回 Shell后面再拆”。所以工程增加了结构门禁scripts/verify-ui-layering.sh。门禁做三类检查shell_lines$(wc-l$SHELL_FILE|tr-d )if[$shell_lines-gt4000];thenechoUI 分层检查失败WorkspaceShell.ets 超过 4000 行上限。2exit1ficomponent_count$(awk/^Component$/ { count 1 } END { print count 0 }$SHELL_FILE)if[$component_count-ne1];thenechoWorkspaceShell.ets 只应声明一个容器组件。2exit1fi它还检查 bridge、model、components 和 panels 的关键文件是否存在并接入统一的verify-local.sh。4000 行不是代码质量分数而是一个回退报警器当前 Shell 为 3551 行预留了合理装配空间但不允许整块展示 UI 无声回流。如果未来新增的确是复杂应用协调逻辑应该先复核 ADR而不是为了绿灯随意调高数字。真实鸿蒙 PC 模拟器验证下面的图片来自 HarmonyOS 6.1.1 API 24 MateBook Pro 2in1 模拟器使用最终 Debug HAP 运行后由snapshot_display直接采集。截图中搜索面板已经作为独立WorkspaceSearchPanel渲染侧栏由拖动条从较窄宽度扩大后三个搜索选项恢复为单行标签、工具栏、预览和状态栏同时保持完整。设备验证不是只看截图。实际执行路径包括启动最终 Debug HAP确认 ArkWeb Bridge 完成加载并显示已有 Markdown 预览。从文件面板切换到大纲面板确认标题数量与跳转行正常显示。切换到搜索面板确认当前文档、工作区、快速打开三个模式和查找替换控件完整。拖动侧栏分隔线约 176 像素确认父容器收到宽度变化、编辑区同步缩放、搜索选项响应式切换。运行模拟器 ohosTest确认文件、恢复、窗口会话、版本历史、搜索、链接和设置服务没有因 UI 移动回退。测试结果与第一次失败的意义最终验证结果如下UI 结构门禁通过WorkspaceShell.ets3551 行只包含一个Component容器声明CommonMark 0.31.2 conforming 配置652/652GFM 冻结扩展语料 30 例通过Web Playwright66/66Debug HAP 构建通过ArkTSUnitTestBuild通过ohosTest HAP 构建通过MateBook Pro 2in1 模拟器 ohosTest16/16Failure 0、Error 0总耗时 2267 ms模拟器搜索面板切换和侧栏拖动通过。验证过程中出现过一次真实失败EditorBridge从model移到bridge后Shell 的导入已经修改但EditorSurface.ets仍指向旧路径。Web 的 66 项测试全部通过随后 ArkTS 编译报出无法解析../model/EditorBridge。这正好说明混合工程不能只跑 Web 测试。修正组件导入后重新执行完整统一验证Web、Debug HAP 和 UnitTestBuild 才全部通过。这个过程没有被从报告中删除因为工程证据的价值不只是最后一行绿色输出。它证明结构调整需要覆盖依赖图的不同编译单元也证明统一脚本应该把 Web、结构检查和 ArkTS 构建串在一起。为什么没有继续拆成更多 Controller分层后 Shell 仍有 3551 行主要是保存、恢复、外部冲突、搜索任务、导出、分享、多窗口和窗口关闭编排。继续拆当然可能让数字更小但这会跨越多个已经稳定的系统能力链路。本次采用 Level 2、D2只拆当前用户明确指出的 UI 所有权问题保持 service 契约和窗口状态模型不变。没有引入全局 store、事件总线、依赖注入或新的 Controller 框架。后续只有在某条业务编排出现第二个真实调用方、难以单元测试或再次逼近结构门禁时才按纵切提取独立控制器。这种克制对鸿蒙 PC 文档软件尤其重要。文件写入、崩溃恢复和多窗口冲突不是适合为了目录美观而大范围搬动的代码。展示层先变清楚系统能力继续稳定是更可验证的顺序。可以复用的判断方法为其他 ArkUI 桌面应用拆分超大页面时可以用四个问题判断代码应该去哪这个状态是否需要跨重绘、重启或窗口恢复需要时由容器或 service 持有只描述悬停和弹层时由组件持有。这个区域是否对应用户能够命名的完整桌面区域如果是它适合作为 panel 或 composite component。这个代码是否接触文件、网络、系统窗口或持久化如果是它不应藏在纯展示面板内部。这个抽象是否减少当前真实的心智负担如果只是把十行代码搬到新文件且没有稳定边界就不值得创建。最终可维护性并不来自文件数量而来自稳定的依赖方向服务提供能力容器组织状态与命令面板和组件展示数据并上报意图Bridge 只连接受限边界。对于优先适配鸿蒙 PC 的 Markdown 编辑器这种层次能够让后续安全、隐私、发布和更多桌面功能继续推进同时避免工作台再次退化成一个任何修改都牵动全局的超大文件。