JGJS2026/docs/project-list-logic-audit.md
2026-03-26 16:15:21 +08:00

10 KiB
Raw Blame History

项目清单与项目切换逻辑审计

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 持久化数据库

关键代码逻辑:

const currentProjectId = pickBootstrapProjectId()
pinia.use(
  piniaPersistedstate({
    name: getProjectDbName(currentProjectId),
    storeName: 'pinia',
    mode: 'multiple'
  })
)

结论:

  • 应用实例一旦启动Pinia 的持久化库已经绑定到某个项目
  • 后续如果只改 URL、不整页重载就不会真正切到目标项目库

这是一条核心约束。


2.4 首页项目列表与进入已有项目

文件:

  • src/features/workbench/components/HomeEntryView.vue

职责:

  • 展示首页已有项目列表
  • 打开已有项目
  • 新建项目
  • 首页导入

当前关键逻辑:

const refreshExistingProjects = async () => {
  const projects = listProjects()
  existingProjects.value = projects.map(project => ({
    id: project.id,
    name: project.name,
    updatedAt: project.updatedAt
  }))
}
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
}
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

职责:

  • 决定显示首页还是工作区
  • 初始化重复项目检测
  • 删除项目后回首页

当前关键逻辑:

if (!isForceHomeRequest.value && currentProjectId.value !== QUICK_PROJECT_ID && currentProjectId.value !== DEFAULT_PROJECT_ID) {
  initCurrentProjectLock()
}
const shouldLockDefaultProject =
  !isForceHomeRequest.value
  && currentProjectId.value === DEFAULT_PROJECT_ID
  && tabStore.hasCompletedSetup
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

关键逻辑:

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
}
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. 逻辑冲突检查

冲突 Adefault 同时承担两种身份

现状:

  • 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.vuehandleProjectDeleted() 监听到删除事件后也会再次重定向

冲突:

  • 同一个业务动作存在两个页面跳转出口
  • 两处都在改 tabStore.hasCompletedSetup
  • 两处都依赖锁释放时序

结果:

  • 更容易和 forceHome、水合、默认项目锁判断形成竞态

结论:

  • 删除当前项目应该收敛成单一出口

冲突 D首页项目列表读的是注册表工作区项目实际名读的是项目库

现状:

  • 首页 refreshExistingProjects() 直接吃 listProjects()
  • App.vue 冲突页会尝试再去项目库里补 xm-base-info-v1.projectName

冲突:

  • 同一个“项目名称”有两套来源:
    • 注册表
    • 项目库

结果:

  • 容易出现列表展示名、实际项目名、最近编辑项目排序不同步

结论:

  • 首页项目列表和冲突页项目列表的名称来源应该统一

冲突 E首页导入链路仍然保留了“只改 URL 不 reload”的旧思路

现状:

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. 最后再回头收敛重复项目检测

否则就是继续在同一组冲突需求上打补丁,回归会反复发生。