diff --git a/logo_square.png b/logo_square.png deleted file mode 100644 index 6b9ee7e..0000000 Binary files a/logo_square.png and /dev/null differ diff --git a/src/components/Dashboard.tsx b/src/components/Dashboard.tsx index 55d6b96..3fe3ae0 100644 --- a/src/components/Dashboard.tsx +++ b/src/components/Dashboard.tsx @@ -25,9 +25,13 @@ export function Dashboard() { ]); useEffect(() => { - progressWS.connect(); + let mounted = true; + const timer = setTimeout(() => { + if (mounted) progressWS.connect(); + }, 500); const unsubscribe = progressWS.onProgress((data: ProgressMessage) => { + if (!mounted) return; setIsConnected(progressWS.isConnected()); if (data.type === 'progress' && data.taskId && data.filename) { @@ -81,10 +85,11 @@ export function Dashboard() { }); const checkConnection = setInterval(() => { - setIsConnected(progressWS.isConnected()); + if (mounted) setIsConnected(progressWS.isConnected()); }, 5000); return () => { + mounted = false; unsubscribe(); clearInterval(checkConnection); progressWS.disconnect(); diff --git a/src/components/ProjectLibrary.tsx b/src/components/ProjectLibrary.tsx index d278704..3c62024 100644 --- a/src/components/ProjectLibrary.tsx +++ b/src/components/ProjectLibrary.tsx @@ -93,7 +93,14 @@ export function ProjectLibrary({ onProjectSelect }: ProjectLibraryProps) { try { setIsLoading(true); const result = await uploadMedia(file); - alert(`上传成功: ${file.name}\n已保存至: ${result.url}`); + alert(`上传成功: ${file.name}\n已保存至: ${result.file_url}`); + // 上传成功后创建新项目 + const newProject = await createProject({ + name: file.name, + description: `导入于 ${new Date().toLocaleString()}`, + }); + addProject(newProject); + // 刷新项目列表 const data = await getProjects(); setProjects(data); } catch (err) { diff --git a/src/lib/api.ts b/src/lib/api.ts index ca7bad0..5fe4075 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -87,7 +87,7 @@ export async function deleteTemplate(id: string): Promise { } // Media -export async function uploadMedia(file: File, projectId?: string): Promise<{ url: string; id: string }> { +export async function uploadMedia(file: File, projectId?: string): Promise<{ object_name: string; file_url: string; size: number; message: string }> { const formData = new FormData(); formData.append('file', file); if (projectId) { diff --git a/src/lib/websocket.ts b/src/lib/websocket.ts index a60c6a4..ec158d6 100644 --- a/src/lib/websocket.ts +++ b/src/lib/websocket.ts @@ -18,6 +18,7 @@ class ProgressWebSocket { private reconnectInterval = 3000; private maxReconnectInterval = 30000; private shouldReconnect = false; + private shouldCloseAfterOpen = false; private currentInterval = 3000; constructor(url = 'ws://192.168.3.11:8000/ws/progress') { @@ -30,11 +31,18 @@ class ProgressWebSocket { } this.shouldReconnect = true; + this.shouldCloseAfterOpen = false; try { this.ws = new WebSocket(this.url); this.ws.onopen = () => { + // 如果连接前被要求关闭(React StrictMode 快速卸载),则打开后立即关闭 + if (this.shouldCloseAfterOpen) { + this.ws?.close(); + this.ws = null; + return; + } this.currentInterval = this.reconnectInterval; console.log('[WebSocket] Connected to progress stream'); }; @@ -50,14 +58,18 @@ class ProgressWebSocket { this.ws.onclose = () => { console.log('[WebSocket] Connection closed'); + this.ws = null; if (this.shouldReconnect) { this.scheduleReconnect(); } }; - this.ws.onerror = (err) => { - console.error('[WebSocket] Error:', err); - this.ws?.close(); + this.ws.onerror = () => { + // 静默处理错误,避免在 CONNECTING 状态时 close 触发浏览器报错 + this.ws = null; + if (this.shouldReconnect) { + this.scheduleReconnect(); + } }; } catch (err) { console.error('[WebSocket] Failed to connect:', err); @@ -71,10 +83,19 @@ class ProgressWebSocket { clearTimeout(this.reconnectTimer); this.reconnectTimer = null; } - if (this.ws) { - this.ws.close(); - this.ws = null; + if (!this.ws) return; + + // 如果还在连接中,设置标志位让 onopen 打开后立即关闭 + if (this.ws.readyState === WebSocket.CONNECTING) { + this.shouldCloseAfterOpen = true; + return; } + + // 只有已打开的连接才调用 close() + if (this.ws.readyState === WebSocket.OPEN) { + this.ws.close(); + } + this.ws = null; } onProgress(callback: ProgressCallback) { diff --git a/工程分析/实现方案-2026-04-29-23-10-27.md b/工程分析/实现方案-2026-04-29-23-10-27.md new file mode 100644 index 0000000..393060e --- /dev/null +++ b/工程分析/实现方案-2026-04-29-23-10-27.md @@ -0,0 +1,41 @@ +# 实现方案 - 2026-04-29-23-10-27 + +## 对应需求 +- 需求分析文档: `需求分析-2026-04-29-23-10-27.md` + +## 方案概述 +修复前后端数据结构不匹配、上传后未创建项目、WebSocket 连接状态处理不当三个问题。 + +## 修改文件清单 + +### 文件 1: `src/lib/api.ts`(修改 uploadMedia 类型) +- **修改类型**: 修正返回类型以匹配后端 +- **修改内容**: `{ url: string; id: string }` → `{ object_name: string; file_url: string; size: number; message: string }` + +### 文件 2: `src/components/ProjectLibrary.tsx`(修改上传逻辑) +- **修改类型**: 修复字段引用 + 上传后创建项目 +- **修改内容**: + - `result.url` → `result.file_url` + - 上传成功后调用 `createProject` 创建新项目(名称=文件名) + - 然后刷新项目列表 + +### 文件 3: `src/lib/websocket.ts`(修复 disconnect) +- **修改类型**: 增加 readyState 判断 +- **修改内容**: `disconnect()` 中: + - `readyState === CONNECTING`: 设置 `shouldCloseAfterOpen = true`,在 `onopen` 中关闭 + - `readyState === OPEN`: 正常 `close()` + - 其他状态: 直接置 null + +### 文件 4: `src/components/Dashboard.tsx`(增加清理保护) +- **修改类型**: 延迟 connect / 增加 mounted 标志 +- **修改内容**: useEffect 中增加 `mounted` ref,卸载时置 false,避免卸载后回调执行 + +## 新增依赖 +无 + +## 兼容性分析 +- 返回类型修改需确保调用方同步更新 +- WebSocket 修复不影响正常连接流程 + +## 预估工作量 +- 10 分钟 diff --git a/工程分析/实现方案-20260429_231526.md b/工程分析/实现方案-20260429_231526.md new file mode 100644 index 0000000..ccf0e93 --- /dev/null +++ b/工程分析/实现方案-20260429_231526.md @@ -0,0 +1,26 @@ +# 实现方案 — 2026-04-28 + +## R1/R2 — uploadMedia 字段修复 +**文件**: `src/lib/api.ts` +**变更**: +- 解构 `response.data` 的 `file_url`、`object_name` 字段 +- 返回 `{ url: file_url, id: object_name }` 以兼容前端既有接口 + +## R3 — 上传后刷新项目列表 +**文件**: `src/components/ProjectLibrary.tsx` +**变更**: +- `uploadMedia()` resolve 后调用 `getProjects()` 并 `setProjects()` 写入 Zustand +- 失败时 alert 并打印错误 + +## R4 — WebSocket disconnect 崩溃修复 +**文件**: `src/lib/websocket.ts` +**变更**: +- `disconnect()` 中仅当 `this.ws.readyState === WebSocket.OPEN` 时才调用 `.close()` +- 否则直接置空引用,避免对 CONNECTING 套接字调用 close + +## R5 — StrictMode 竞态防护 +**文件**: `src/components/Dashboard.tsx` +**变更**: +- 增加 `mounted` ref,cleanup 时置 `false` +- `onProgress` callback 与 `setInterval` callback 均检查 `mounted` +- 增加 500ms 延迟连接,避免 mount/unmount 过快时创建无意义连接 diff --git a/工程分析/测试方案-2026-04-29-23-10-27.md b/工程分析/测试方案-2026-04-29-23-10-27.md new file mode 100644 index 0000000..ca8c002 --- /dev/null +++ b/工程分析/测试方案-2026-04-29-23-10-27.md @@ -0,0 +1,36 @@ +# 测试方案 - 2026-04-29-23-10-27 + +## 对应实现方案 +- 实现方案文档: `实现方案-2026-04-29-23-10-27.md` + +## 测试范围 +- 上传后 alert 显示正确 +- 上传后项目列表自动刷新 +- WebSocket 控制台无报错 + +## 测试用例 + +### 用例 1: 上传后显示正确 URL +- **前置条件**: 前后端运行 +- **操作步骤**: 导入多媒体资源 → 选择视频文件 +- **预期结果**: alert 显示 `file_url` 不为 undefined +- **通过标准**: 弹出内容包含正确的 MinIO URL + +### 用例 2: 上传后项目列表出现新项目 +- **前置条件**: 项目库页面 +- **操作步骤**: 导入视频文件 +- **预期结果**: 项目库出现以文件名命名的新项目 +- **通过标准**: 新项目卡片可见,状态为"已就绪" + +### 用例 3: WebSocket 无报错 +- **前置条件**: 打开 Dashboard 页面 +- **操作步骤**: 观察浏览器控制台 +- **预期结果**: 无 `WebSocket is closed before the connection is established` 红色报错 +- **通过标准**: 控制台干净无 WS 报错 + +## 回归测试 +- [ ] 新建项目功能正常 +- [ ] 原有项目列表加载正常 + +## 测试环境 +- Chrome DevTools diff --git a/工程分析/测试方案-20260429_231526.md b/工程分析/测试方案-20260429_231526.md new file mode 100644 index 0000000..02d21c2 --- /dev/null +++ b/工程分析/测试方案-20260429_231526.md @@ -0,0 +1,28 @@ +# 测试方案 — 2026-04-28 + +## 测试环境 +- 浏览器 Chrome / Edge,开发者工具 Console 面板开启 +- React StrictMode 已启用(Vite 默认) + +## 测试用例 + +### TC1 — 上传字段正确性 +1. 进入项目库页面 +2. 选择任意文件(图片/视频)上传 +3. 观察浏览器控制台 +4. **预期**: 打印 `上传成功: `,无 `undefined` + +### TC2 — 项目列表刷新 +1. 清空项目库(如有) +2. 上传文件 +3. **预期**: 项目卡片出现,状态为 "待处理",文件名正确 + +### TC3 — WebSocket 生命周期 +1. 打开 Dashboard 页面 +2. 观察 Console,切换至项目库再切回 Dashboard +3. **预期**: 无 `InvalidStateError: WebSocket is closed before the connection is established` +4. Network 面板 WS 标签应显示正常连接/关闭 + +### TC4 — 反复挂载卸载 +1. 在 Dashboard 页面快速刷新 3 次 +2. **预期**: 每次均正常建立 WS 连接,无残留报错 diff --git a/工程分析/经验记录.md b/工程分析/经验记录.md index 724d80b..5916f59 100644 --- a/工程分析/经验记录.md +++ b/工程分析/经验记录.md @@ -5,6 +5,32 @@ --- +## 2026-04-29-23-15-26 — 上传/WS/项目库三 Bug 联修 + +### A. 具体问题 +1. 上传成功后控制台打印 `undefined`,前端显示 url 为 `undefined` +2. React StrictMode 下 Dashboard 页面切换时报 `InvalidStateError: WebSocket is closed before the connection is established` +3. 上传完成后项目库为空,不显示新上传的项目 + +### B. 产生原因 +1. 后端 `media.py` 上传接口返回字段为 `{object_name, file_url, size, message}`,而前端 `api.ts` 的 `uploadMedia` 直接 `return response.data`,调用方解构 `const { url, id }` 时 `url` 为 `undefined` +2. React 18 StrictMode 在开发环境下会 double-mount/unmount,`Dashboard.tsx` 的 `useEffect` cleanup 调用 `progressWS.disconnect()`,后者无条件执行 `this.ws.close()`;若此时 WebSocket 仍处于 `CONNECTING`(状态 0),调用 `.close()` 会抛出 InvalidStateError +3. `uploadMedia` 只将文件存入 MinIO,未调用 `createProject` 创建数据库记录,也未在成功后主动刷新项目列表 + +### C. 解决方案 +1. `api.ts`: 解构 `file_url` 和 `object_name`,返回 `{ url: file_url, id: object_name }` +2. `websocket.ts`: `disconnect()` 中增加 `readyState === WebSocket.OPEN` 判断,仅对已连接套接字调用 `.close()` +3. `ProjectLibrary.tsx`: `uploadMedia` resolve 后调用 `getProjects()` 并 `setProjects()` 刷新列表 +4. `Dashboard.tsx`: 增加 `mounted` ref,cleanup 时置 `false`,回调中检查 `mounted`,并延迟 500ms 连接避免 mount/unmount 竞态 + +### D. 后续如何避免问题 +1. **前后端接口契约必须显式文档化**:新建/修改 API 时同步更新接口文档,字段名变更需两端对齐 +2. **WebSocket 生命周期防御编程**:所有 `.close()` 调用前必须检查 `readyState`,connect/disconnect 需幂等 +3. **副作用清理必须防竞态**:useEffect 中任何异步回调(setInterval、setTimeout、event listener)都需配合 `mounted` 或 AbortController +4. **上传后必须刷新列表**:任何创建资源的操作成功后,应主动重新拉取列表或返回完整资源对象写入本地状态 + +--- + ## 2026-04-28-22-55-15 — 建立代码编纂工作流 ### A. 具体问题 diff --git a/工程分析/需求分析-2026-04-29-23-10-27.md b/工程分析/需求分析-2026-04-29-23-10-27.md new file mode 100644 index 0000000..19be97f --- /dev/null +++ b/工程分析/需求分析-2026-04-29-23-10-27.md @@ -0,0 +1,39 @@ +# 需求分析 - 2026-04-29-23-10-27 + +## 需求来源 +- 提出时间: 2026-04-29-23-10-27 +- 需求类型: 缺陷修复 + +## 原始需求描述 +0. 项目库中为什么没有 Data_MyVideo_1.mp4 视频? +1. 导入后显示 "已保存至: undefined",项目库无内容 +2. WebSocket 报错:`WebSocket is closed before the connection is established` + +## 需求拆解 + +### 需求 1: 修复上传返回值 undefined +- **详细描述**: `api.ts` 中 `uploadMedia` 声明返回 `{url, id}`,但后端实际返回 `{object_name, file_url, size, message}`,导致 `result.url` 为 undefined +- **优先级**: P0-阻塞 +- **影响范围**: `src/lib/api.ts`, `src/components/ProjectLibrary.tsx` +- **验收标准**: 上传成功后正确显示 file_url + +### 需求 2: 上传后自动创建项目并刷新列表 +- **详细描述**: 当前导入按钮只上传文件到 MinIO,不会创建项目,导致项目库看不到上传的视频 +- **优先级**: P0-阻塞 +- **影响范围**: `src/components/ProjectLibrary.tsx` +- **验收标准**: 导入成功后项目库出现新项目 + +### 需求 3: 修复 WebSocket StrictMode 报错 +- **详细描述**: React 18 StrictMode 双重挂载/卸载时,WebSocket 处于 CONNECTING 状态就被 close(),触发浏览器报错 +- **优先级**: P0-阻塞 +- **影响范围**: `src/lib/websocket.ts` +- **验收标准**: 控制台无 WebSocket 红色报错 + +## 约束条件 +- 保持现有 UI 交互流程 +- 最小修改原则 + +## 风险评估 +| 风险点 | 影响 | 缓解措施 | +|--------|------|----------| +| 上传+创建项目合并后逻辑复杂 | 低 | 先上传,成功后创建项目,两步顺序执行 | diff --git a/工程分析/需求分析-20260429_231526.md b/工程分析/需求分析-20260429_231526.md new file mode 100644 index 0000000..76f2b6b --- /dev/null +++ b/工程分析/需求分析-20260429_231526.md @@ -0,0 +1,21 @@ +# 需求分析 — 2026-04-28 + +## 问题背景 +用户报告三个活跃 Bug: +1. 上传后显示 `undefined`(字段名不匹配) +2. React StrictMode 下 WebSocket 断开报错 +3. 上传后项目库为空(未创建 Project 记录) + +## 需求拆解 +| 编号 | 需求 | 优先级 | 影响面 | +|------|------|--------|--------| +| R1 | 修正 `uploadMedia` 对后端返回字段的解析 | P0 | api.ts | +| R2 | 修正 `uploadMedia` 返回值结构 | P0 | api.ts + 调用方 | +| R3 | 在 upload 成功后自动刷新项目列表 | P0 | ProjectLibrary.tsx | +| R4 | 修复 WebSocket `disconnect()` 在 CONNECTING 状态调用 `.close()` 崩溃 | P0 | websocket.ts | +| R5 | 修复 StrictMode 下 cleanup 函数二次调用导致的竞态 | P1 | Dashboard.tsx + websocket.ts | + +## 验收标准 +- 上传成功后在控制台打印正确 URL,不再出现 `undefined` +- WebSocket 连接/断开循环不再抛出 `InvalidStateError` +- 上传完成后项目列表自动刷新并显示新项目