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