原生鸿蒙像素画板实战 16:多项目管理 单个像素画布能保存并不意味着编辑器已经具备项目管理。用户真正需要的是新建一个项目不会覆盖旧画布切换项目前会把当前修改收束重命名不会让列表和当前标题不一致删除当前项目后还能自然回到一个可用状态。bitArt 把这些动作放在 EditorProjectWorkflowService 中页面只负责打开管理面板和传入用户选择。项目切换的第一原则是先收束当前会话如果项目切换时直接 loadProject正在进行的笔触可能还没有结束自动保存队列也可能没有把最新像素写盘。用户看到的现象是切回来少了一笔或者项目列表的更新时间不对。删除当前项目也不能只删文件当前页面仍持有旧 projectId下一次保存会把一个“已删除项目”写回去。项目面板和持久化服务如何配合项目管理的关键不是列出一堆文件而是让当前会话能安全交给下一份项目。笔触、动画和保存队列需要在切换前被明确收束。管理模式和打开模式不是同一个点击语义当前打开项目的流程先判断是否处于管理模式。管理模式下点击卡片只切换选中状态不加载项目普通模式下如果点击当前项目则关闭面板点击其他项目才依次停止动画、结束笔触、保存当前项目、读取目标项目、应用持久化状态、重新安排自动保存并刷新摘要。这个顺序把“项目切换”从一个跳转动作变成一个带保护的会话交接。项目名称也有独立的草稿字段。提交时先用 EditorPersistenceService.normalizeProjectName 取得有效名称再根据它是不是当前项目决定走 saveNow 还是单独 renameProject。这样列表里的非当前项目不需要被完整加载就能改名。切换项目的顺序必须先结束当前会话再读取目标项目。static async openProject(target: EditorProjectWorkflowTarget, context: common.UIAbilityContext | null, projectId: string, hooks: EditorProjectWorkflowHooks): Promisevoid { if (context null || projectId.length 0) return; if (target.projectManageMode) { hooks.toggleProjectSelection(projectId); return; } if (projectId target.currentProjectId) { target.projectManagerOpen false; return; } hooks.stopAnimationPlayback(); hooks.endStroke(); await hooks.saveNow(); const project await EditorPersistenceService.loadProject(context, projectId); if (project ! null) { hooks.applyPersistentProject(project); hooks.scheduleAutoSave(); target.projectManagerOpen false; await hooks.refreshProjectSummaries(); } }重命名与删除都需要明确的后续状态项目管理中最重要的不是循环而是状态机。一个项目卡片在普通模式代表“打开”在管理模式代表“选择”删除动作完成后如果删掉的是当前项目工作流要从刷新后的摘要中打开第一个剩余项目若一个也没有则创建默认项目。每一步都有明确的来源和去向因此不会把空 projectId 留给后续导出或保存。const fallbackName EditorProjectPanelService.getProjectNameFallback(target.projectSummaries, projectId, target.canvasWidth, target.canvasHeight); const normalizedName EditorPersistenceService.normalizeProjectName(target.editingProjectNameText, fallbackName); if (EditorProjectPanelService.updateProjectNameDraft(target, projectId, normalizedName)) { hooks.markProjectDirty(); } if (projectId target.currentProjectId) { target.currentProjectName normalizedName; await hooks.saveNow(true); } else { await EditorPersistenceService.renameProject(context, projectId, normalizedName); }把当前会话交给下一个项目之前saveCurrentProject、openProjectManager 与 openProject 都先收束动画和笔触。项目面板的 selectedProjectIds 只服务于批量操作currentProjectId 才是当前工作区的唯一来源。刷新摘要后再关闭面板可以避免 UI 先关闭却仍显示旧列表的短暂错位。项目摘要、当前项目 ID 和管理模式各有职责刷新摘要不能替代应用实际项目数据。const deletingCurrent target.selectedProjectIds.indexOf(target.currentProjectId) 0; await hooks.saveNow(true); await EditorProjectActionService.deleteProjects(context, target.selectedProjectIds.slice()); target.selectedProjectIds []; await hooks.refreshProjectSummaries(); if (deletingCurrent) { await EditorProjectWorkflowService.openFallbackProjectAfterDeletion(target, context, defaultCanvasSize, hooks); }项目管理不能省略的保护步骤切换项目前先结束笔触和保存不能依赖页面销毁时的偶然写盘。管理模式下的点击只能改变 selectedProjectIds不能同时打开项目。删除当前项目后必须立即选择后备项目或创建默认项目不能留下悬空 currentProjectId。项目管理的价值在于把“可编辑状态”从页面生命周期里拿出来。用户可以创建多个尺寸不同的画布保存它们各自的图层、帧、调色板和设置之后在管理器里切换而不需要重新初始化工作区。对于创作工具这比单纯加一个“新建”按钮更接近真实使用场景。用两个项目验证切换顺序多项目管理最适合用“两个不同状态”的对照实验验证。项目 A 使用 32×64 画布、两层和一个自定义色项目 B 使用 64×64 画布、三层和不同活动帧。在 A 中完成一笔还未显式点击保存的绘制后切到 B再回到 A检查像素、尺寸、图层顺序、当前工具和颜色。若任一字段回到默认值说明保存或 applyPersistentProject 的边界不完整。删除路径也不能只看文件是否消失。删除非当前项目后当前工作区应稳定删除当前项目后应自动进入另一个可用项目删除最后一个项目时应产生默认项目而不是空白页面。项目管理的好体验来自这些后续状态而不是管理器里那个删除按钮本身。把这组回归固定下来后续加导入、合并或批量操作时也能及时发现会话交接被破坏。怎样确认项目切换不会丢修改项目检查应覆盖当前项目与非当前项目两条路径尤其要验证删除后的后备状态。建立两个不同尺寸、不同图层数的项目切换前后确认每个项目保留自己的像素和活动图层。在管理模式中点按多个项目确认只改变选中状态不改变当前画布。重命名当前项目与非当前项目关闭并重开管理器确认名称一致。删除当前项目、删除非当前项目和删除最后一个项目确认都有明确的后续工作区。项目管理检查记录 切换前收束笔触并保存 管理模式只选择不打开 改名后摘要和当前标题一致 删除当前项目后有可编辑的后备项目本地项目管理的范围当前项目管理针对单设备本地项目不包含多人协作、云端版本分支或跨设备冲突解决。项目文件导入导出提供了可迁移入口但不等同于实时同步。这一篇的重点是把项目切换做成一次安全的会话交接。项目可以并列存在之后下一篇继续处理更难的情况怎样把多个项目合成一个新项目而不覆盖原稿。