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

428 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 项目清单与项目切换逻辑审计
## 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. 最后再回头收敛重复项目检测
否则就是继续在同一组冲突需求上打补丁,回归会反复发生。