# 项目清单与项目切换逻辑审计 ## 1. 审计目标 本文件用于梳理“项目清单 / 首页进入项目 / 删除当前项目 / 重复项目检测”相关代码,并检查需求逻辑是否存在冲突。 当前问题已经表现为一组相互影响的回归: 1. 首页打开已有项目时,可能进入了错误项目 2. 删除当前项目后,回首页可能误触发“检测到项目重复打开” 3. 两个相同项目 tab 的重复检测一度失效 这些问题不是孤立 bug,而是同一组状态源和时序设计在互相冲突。 --- ## 2. 相关代码清单 ### 2.1 项目注册表 文件: - `src/lib/projectRegistry.ts` 职责: - 维护项目元数据列表 - 提供 `listProjects / createProject / upsertProject / deleteProject` - 首页项目列表、项目菜单、冲突页项目列表都依赖这里 关键事实: - `listProjects()` 按 `lastOpenedAt` 排序 - 注册表只存元信息,不负责切换实际项目数据上下文 --- ### 2.2 URL 与项目上下文 文件: - `src/lib/workspace.ts` 职责: - `readCurrentProjectId()` - `ensureProjectIdInUrl()` - `buildProjectUrl()` - `DEFAULT_PROJECT_ID = 'default'` - `QUICK_PROJECT_ID = 'quick'` 关键事实: - `projectId` 主要通过 URL 传递 - `default` 同时承担“首页占位项目”语义 --- ### 2.3 应用启动时绑定项目库 文件: - `src/main.ts` - `src/pinia/Plugin/indexdb.ts` 职责: - 启动时根据当前 `projectId` 绑定 Pinia 持久化数据库 关键代码逻辑: ```ts const currentProjectId = pickBootstrapProjectId() pinia.use( piniaPersistedstate({ name: getProjectDbName(currentProjectId), storeName: 'pinia', mode: 'multiple' }) ) ``` 结论: - 应用实例一旦启动,Pinia 的持久化库已经绑定到某个项目 - 后续如果只改 URL、不整页重载,就不会真正切到目标项目库 这是一条核心约束。 --- ### 2.4 首页项目列表与进入已有项目 文件: - `src/features/workbench/components/HomeEntryView.vue` 职责: - 展示首页已有项目列表 - 打开已有项目 - 新建项目 - 首页导入 当前关键逻辑: ```ts const refreshExistingProjects = async () => { const projects = listProjects() existingProjects.value = projects.map(project => ({ id: project.id, name: project.name, updatedAt: project.updatedAt })) } ``` ```ts const navigateToWorkspace = (projectIdRaw: string, mode: 'project' | 'quick') => { const projectId = String(projectIdRaw || '').trim() writeWorkspaceMode(mode) const currentProjectId = getActiveProjectId() const shouldReloadApp = currentProjectId !== projectId if (shouldReloadApp) { window.location.href = buildProjectUrl(projectId, { forceHome: false, newProject: false }) return false } writeCleanProjectUrl(projectId) return true } ``` ```ts const enterExistingProject = (projectIdRaw: string) => { const projectId = String(projectIdRaw || '').trim() upsertProject(projectId, resolveProjectRegistryName(projectId)) if (!navigateToWorkspace(projectId, 'project')) return tabStore.enterWorkspace(...) tabStore.hasCompletedSetup = true } ``` 结论: - 当前首页进入已有项目,已经开始依赖“必要时整页重载”来切项目库 - 这条方向是对的,因为它符合 `main.ts` 的启动约束 --- ### 2.5 App 级首页/工作区/冲突锁 文件: - `src/App.vue` - `src/lib/projectSessionLock.ts` 职责: - 决定显示首页还是工作区 - 初始化重复项目检测 - 删除项目后回首页 当前关键逻辑: ```ts if (!isForceHomeRequest.value && currentProjectId.value !== QUICK_PROJECT_ID && currentProjectId.value !== DEFAULT_PROJECT_ID) { initCurrentProjectLock() } ``` ```ts const shouldLockDefaultProject = !isForceHomeRequest.value && currentProjectId.value === DEFAULT_PROJECT_ID && tabStore.hasCompletedSetup ``` ```ts watch( () => tabStore.hasCompletedSetup, (next) => { if (!next) return if (isForceHomeRequest.value) return if (currentProjectId.value !== DEFAULT_PROJECT_ID) return initCurrentProjectLock() } ) ``` 结论: - `App.vue` 现在在尝试区分三种状态: - 真实项目 - 首页占位态 - `default` 进入工作区后的实际项目态 - 这套设计复杂,而且和 `default` 复用语义强耦合 --- ### 2.6 删除当前项目 文件: - `src/layout/tab.vue` - `src/App.vue` 关键逻辑: ```ts if (isCurrentProject) { window.dispatchEvent(new CustomEvent('jgjs-release-project-lock')) await clearProjectPersistence() await deleteIndexedDBByName(getProjectDbName(project.id)) const removed = deleteProject(project.id) emitProjectDeleted(project.id) tabStore.resetTabs() tabStore.hasCompletedSetup = false window.location.href = buildProjectUrl('default', { forceHome: true }) return } ``` ```ts const handleProjectDeleted = (deletedProjectId: string) => { handleReleaseProjectLock() tabStore.resetTabs() tabStore.hasCompletedSetup = false const href = buildProjectUrl(DEFAULT_PROJECT_ID, { forceHome: true }) window.setTimeout(() => { window.location.href = href }, 120) } ``` 结论: - 删除当前项目时,既有 tab 内直接重定向,又有 `App.vue` 监听删除事件后再次重定向 - 这两条链路在行为上相近,但不是单一出口 --- ## 3. 需求逻辑清单 下面是业务上应该同时成立的需求: 1. 首页只是项目入口,不应触发“重复项目打开”拦截 2. 打开已有项目时,必须进入用户点选的那个项目,不能串到别的项目 3. 删除当前项目后,必须稳定回首页,不能落到冲突页 4. 两个浏览器 tab 打开同一个真实项目时,必须触发重复项目拦截 5. `quick` 项目与普通项目互不干扰 6. `default` 不能既当“首页占位态”,又在多条链路里被当成“真实项目” --- ## 4. 逻辑冲突检查 ## 冲突 A:`default` 同时承担两种身份 现状: - `default` 既是首页占位项目 - 又可能进入实际工作区 - 锁逻辑还试图区分“default 首页态”和“default 工作区态” 冲突: - 首页不该被锁 - 工作区里的真实项目应该被锁 - 但二者共用同一个 `projectId = default` 结果: - 只要某条链路判断稍有偏差,就会出现: - 首页误触发冲突 - 或重复项目检测失效 结论: - 这是当前最核心的设计冲突 --- ## 冲突 B:项目切换依赖 URL,但持久化库绑定依赖应用启动 现状: - `projectId` 在 URL 中切换 - Pinia 持久化数据库在 `main.ts` 启动时绑定 冲突: - SPA 式改 URL,不会重建持久化上下文 - 整页刷新才能真正切项目库 结果: - 首页打开已有项目,如果只是 `replaceState`,会出现“地址栏变了,项目数据没变” 结论: - 项目切换要么强制 reload,要么重构持久化层支持运行时切库 - 两者不能混用 --- ## 冲突 C:删除当前项目存在双重跳转出口 现状: - `tab.vue` 删除当前项目后会直接 `window.location.href = buildProjectUrl('default', { forceHome: true })` - `App.vue` 的 `handleProjectDeleted()` 监听到删除事件后也会再次重定向 冲突: - 同一个业务动作存在两个页面跳转出口 - 两处都在改 `tabStore.hasCompletedSetup` - 两处都依赖锁释放时序 结果: - 更容易和 `forceHome`、水合、默认项目锁判断形成竞态 结论: - 删除当前项目应该收敛成单一出口 --- ## 冲突 D:首页项目列表读的是注册表,工作区项目实际名读的是项目库 现状: - 首页 `refreshExistingProjects()` 直接吃 `listProjects()` - `App.vue` 冲突页会尝试再去项目库里补 `xm-base-info-v1.projectName` 冲突: - 同一个“项目名称”有两套来源: - 注册表 - 项目库 结果: - 容易出现列表展示名、实际项目名、最近编辑项目排序不同步 结论: - 首页项目列表和冲突页项目列表的名称来源应该统一 --- ## 冲突 E:首页导入链路仍然保留了“只改 URL 不 reload”的旧思路 现状: ```ts const confirmHomeImport = () => { ... const targetProjectId = ... writeCleanProjectUrl(targetProjectId) writeWorkspaceMode('project') tabStore.enterWorkspace(...) tabStore.hasCompletedSetup = true } ``` 冲突: - 这条链路与“打开已有项目时必须整页切库”的新规则不一致 结果: - 同类问题可能在首页导入上继续复现 结论: - 这条链路与 `navigateToWorkspace()` 的规则不一致,属于明确冲突 --- ## 5. 结论 当前项目清单与项目切换逻辑的核心冲突不是单点 bug,而是以下三条设计同时存在: 1. `default` 被复用为“首页占位项目 + 真实工作区项目” 2. 项目切换有时 reload,有时只改 URL,规则不统一 3. 删除/回首页/重复项目检测有多处出口,状态源不唯一 这三条不拆开,任何局部修复都会互相打架。 --- ## 6. 建议的需求基线 建议先对齐以下基线,再继续改代码: 1. 首页不是项目,不参与项目锁 2. 真实项目必须有真实 `projectId`,不要再让 `default` 进入工作区承担真实项目角色 3. 项目切换一律走“整页重启切库”,除非后续重构 Pinia 持久化支持运行时切库 4. 删除当前项目后的回首页只保留一个跳转出口 5. 首页项目列表、冲突页项目列表统一同一套项目名称读取逻辑 --- ## 7. 当前最值得先改的一条 如果只允许先做一条,我建议优先处理: **不要再让 `default` 作为真实项目进入工作区。** 原因: - 这是首页误触发冲突和重复项目检测失灵的共同根因之一 - 也是删除当前项目回首页后最容易炸的那条链路 --- ## 8. 下一步执行建议 下一步不建议继续在现有条件分支上微调。建议按以下顺序重构: 1. 把“首页态”和“真实项目态”彻底分离 2. 把首页进入已有项目、首页导入、工作区打开项目统一成“切项目必须重载应用” 3. 把删除当前项目后的跳转收敛成单一出口 4. 最后再回头收敛重复项目检测 否则就是继续在同一组冲突需求上打补丁,回归会反复发生。