10 KiB
10 KiB
项目清单与项目切换逻辑审计
1. 审计目标
本文件用于梳理“项目清单 / 首页进入项目 / 删除当前项目 / 重复项目检测”相关代码,并检查需求逻辑是否存在冲突。
当前问题已经表现为一组相互影响的回归:
- 首页打开已有项目时,可能进入了错误项目
- 删除当前项目后,回首页可能误触发“检测到项目重复打开”
- 两个相同项目 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.tssrc/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.vuesrc/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.vuesrc/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. 需求逻辑清单
下面是业务上应该同时成立的需求:
- 首页只是项目入口,不应触发“重复项目打开”拦截
- 打开已有项目时,必须进入用户点选的那个项目,不能串到别的项目
- 删除当前项目后,必须稳定回首页,不能落到冲突页
- 两个浏览器 tab 打开同一个真实项目时,必须触发重复项目拦截
quick项目与普通项目互不干扰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”的旧思路
现状:
const confirmHomeImport = () => {
...
const targetProjectId = ...
writeCleanProjectUrl(targetProjectId)
writeWorkspaceMode('project')
tabStore.enterWorkspace(...)
tabStore.hasCompletedSetup = true
}
冲突:
- 这条链路与“打开已有项目时必须整页切库”的新规则不一致
结果:
- 同类问题可能在首页导入上继续复现
结论:
- 这条链路与
navigateToWorkspace()的规则不一致,属于明确冲突
5. 结论
当前项目清单与项目切换逻辑的核心冲突不是单点 bug,而是以下三条设计同时存在:
default被复用为“首页占位项目 + 真实工作区项目”- 项目切换有时 reload,有时只改 URL,规则不统一
- 删除/回首页/重复项目检测有多处出口,状态源不唯一
这三条不拆开,任何局部修复都会互相打架。
6. 建议的需求基线
建议先对齐以下基线,再继续改代码:
- 首页不是项目,不参与项目锁
- 真实项目必须有真实
projectId,不要再让default进入工作区承担真实项目角色 - 项目切换一律走“整页重启切库”,除非后续重构 Pinia 持久化支持运行时切库
- 删除当前项目后的回首页只保留一个跳转出口
- 首页项目列表、冲突页项目列表统一同一套项目名称读取逻辑
7. 当前最值得先改的一条
如果只允许先做一条,我建议优先处理:
不要再让 default 作为真实项目进入工作区。
原因:
- 这是首页误触发冲突和重复项目检测失灵的共同根因之一
- 也是删除当前项目回首页后最容易炸的那条链路
8. 下一步执行建议
下一步不建议继续在现有条件分支上微调。建议按以下顺序重构:
- 把“首页态”和“真实项目态”彻底分离
- 把首页进入已有项目、首页导入、工作区打开项目统一成“切项目必须重载应用”
- 把删除当前项目后的跳转收敛成单一出口
- 最后再回头收敛重复项目检测
否则就是继续在同一组冲突需求上打补丁,回归会反复发生。