From a07e6e4e98bb337926d1c24be411a8a0682d6607 Mon Sep 17 00:00:00 2001 From: admin <572701190@qq.com> Date: Thu, 16 Apr 2026 17:15:37 +0800 Subject: [PATCH] backup before modification at 2026-04-16-17-15-37 --- src/pages/ReportEditor.tsx | 41 +++++++-- 工程分析/实现方案-2026-04-16-17-07-04.md | 102 +++++++++++++++++++++++ 工程分析/测试方案-2026-04-16-17-07-04.md | 88 +++++++++++++++++++ 工程分析/经验记录.md | 20 +++++ 工程分析/需求分析-2026-04-16-17-07-04.md | 26 ++++++ 5 files changed, 269 insertions(+), 8 deletions(-) create mode 100644 工程分析/实现方案-2026-04-16-17-07-04.md create mode 100644 工程分析/测试方案-2026-04-16-17-07-04.md create mode 100644 工程分析/需求分析-2026-04-16-17-07-04.md diff --git a/src/pages/ReportEditor.tsx b/src/pages/ReportEditor.tsx index 3a4306b..a262a28 100644 --- a/src/pages/ReportEditor.tsx +++ b/src/pages/ReportEditor.tsx @@ -507,21 +507,38 @@ export default function ReportEditor() { e.dataTransfer.setData('frameId', frame.id.toString()); }; + const fillPlaceholder = (placeholder: HTMLElement, frame: CapturedFrame) => { + placeholder.innerHTML = ` + × + + `; + placeholder.classList.add('has-image'); + if (editorRef.current) contentRef.current = editorRef.current.innerHTML; + saveDraftToStorage(); + }; + const handleDrop = (e: React.DragEvent, placeholder: HTMLElement) => { e.preventDefault(); const frameId = e.dataTransfer.getData('frameId'); const frame = capturedFrames.find(f => f.id.toString() === frameId); if (frame) { - placeholder.innerHTML = ` - × - - `; - placeholder.classList.add('has-image'); - if (editorRef.current) contentRef.current = editorRef.current.innerHTML; - saveDraftToStorage(); + fillPlaceholder(placeholder, frame); } }; + const insertFrameToPlaceholder = (frame: CapturedFrame) => { + if (!editorRef.current) { + alert('编辑器未准备好'); + return; + } + const emptyPlaceholder = editorRef.current.querySelector('.image-placeholder:not(.has-image)') as HTMLElement | null; + if (!emptyPlaceholder) { + alert('没有可插入图片的空位'); + return; + } + fillPlaceholder(emptyPlaceholder, frame); + }; + const seekToFrame = (frame: CapturedFrame) => { if (!videoRef.current) return; if (frame.videoIndex !== currentVideoIndex) { @@ -1302,7 +1319,15 @@ export default function ReportEditor() {
{frame.timeFormatted} - 可拖拽 +
+ + 可拖拽 +
+ 可拖拽 + + +``` + +按钮使用 `opacity-0 group-hover:opacity-100 transition-opacity`,与 "可拖拽" 显隐行为完全一致。`e.stopPropagation()` 避免触发卡片的 `onClick`(即跳转到视频位置)。 + +--- + +## 风险点及应对策略 + +| 风险 | 影响 | 应对策略 | +|------|------|----------| +| `editorRef.current` 为空时点击插入 | JS 报错 | 函数开头增加判空并 alert 提示 | +| 没有空占位符时点击插入 | 用户困惑 | 未找到 `.image-placeholder:not(.has-image)` 时弹出友好提示 | +| 按钮点击触发卡片 onClick | 视频意外跳转 | 使用 `e.stopPropagation()` 阻止冒泡 | +| `handleDrop` 抽离后功能回退 | 拖拽失效 | 保持 `handleDrop` 调用 `fillPlaceholder`,逻辑与原来一致 | + +--- + +## 改动范围总结 +- 仅修改 `src/pages/ReportEditor.tsx`,不触及其他文件。 +- 不引入新依赖。 diff --git a/工程分析/测试方案-2026-04-16-17-07-04.md b/工程分析/测试方案-2026-04-16-17-07-04.md new file mode 100644 index 0000000..ae04aa7 --- /dev/null +++ b/工程分析/测试方案-2026-04-16-17-07-04.md @@ -0,0 +1,88 @@ +# 测试方案 — 2026-04-16-17-07-04 + +## 测试环境准备 +1. 项目已构建通过,无类型错误。 +2. 使用测试账号 `admin / 123456`(超级管理员)登录系统。 +3. 进入 **图文报告生成** 页面,确认编辑器中已加载默认模板(包含若干 `image-placeholder` 图片占位符)。 +4. 在 **视频分析** 面板中上传至少一个手术视频,等待系统自动抽帧完成。 + +--- + +## 测试项清单 + +### 测试项 1:"插入" 按钮显隐行为正确 +**测试步骤**: +1. 在视频分析面板中查看自动抽帧的关键帧列表。 +2. 将鼠标移入某张关键帧截图卡片上。 +3. 再将鼠标移出该卡片。 + +**预期结果**: +- 鼠标移入时,截图卡片底部同时显示 **"插入"** 和 **"可拖拽"** 两个提示文字。 +- 鼠标移出时,两者同时隐藏。 +- "插入" 位于截图时间(如 `00:15`)和 "可拖拽" 之间。 + +--- + +### 测试项 2:点击 "插入" 自动填充第一个空置占位符 +**测试步骤**: +1. 确认编辑器中存在至少一个尚未填充图片的 `.image-placeholder`(未显示图片的灰色虚线框)。 +2. 在视频分析面板中,将鼠标悬停到第一张关键帧截图上,点击 **"插入"**。 +3. 再次点击第二张关键帧截图的 **"插入"**。 + +**预期结果**: +- 第一次点击后,编辑器中**第一个**空置占位符被填充为第一张关键帧图片。 +- 第二次点击后,编辑器中**第二个**空置占位符被填充为第二张关键帧图片。 +- 填充后的占位符变为正常图片显示,且带有删除按钮 `×`。 + +--- + +### 测试项 3:无空占位符时给出提示 +**测试步骤**: +1. 连续点击 "插入" 直到编辑器中所有 `image-placeholder` 都被填满。 +2. 再次点击任意关键帧的 **"插入"**。 + +**预期结果**: +- 浏览器弹出提示框:**"没有可插入图片的空位"**。 +- 不会报错,现有已填充的图片不受影响。 + +--- + +### 测试项 4:点击 "插入" 不触发视频跳转 +**测试步骤**: +1. 点击某张关键帧卡片的 **"插入"** 按钮。 +2. 观察左侧视频播放器。 + +**预期结果**: +- 视频播放器的当前时间**不发生变化**(不会因为点击了卡片本身而跳转到该帧位置)。 + +--- + +### 测试项 5:原有拖拽功能保持正常 +**测试步骤**: +1. 手动拖拽视频分析面板中的某张关键帧截图到编辑器中的空置 `image-placeholder` 上。 + +**预期结果**: +- 拖拽释放后,该占位符正确显示被拖拽的图片。 +- 拖拽功能与 "插入" 按钮功能互不干扰。 + +--- + +### 测试项 6:构建与类型检查回归 +**测试步骤**: +1. 在项目根目录执行: + ```bash + npm run lint + npm run build + ``` + +**预期结果**: +- `npm run lint` 无 TypeScript 编译错误。 +- `npm run build` 构建成功。 + +--- + +## 回归验证范围 +- [ ] `SystemSettings.tsx` 未被修改,系统设置功能保持正常。 +- [ ] `storage.ts` 未被修改,存储读写保持正常。 +- [ ] 编辑器打印、保存草稿、完成报告功能保持正常。 +- [ ] 关键帧的 "手动截取"、删除功能保持正常。 diff --git a/工程分析/经验记录.md b/工程分析/经验记录.md index 5038a8c..0879246 100644 --- a/工程分析/经验记录.md +++ b/工程分析/经验记录.md @@ -22,3 +22,23 @@ - 在前端使用 contentEditable 的自动保存机制时,保存和恢复草稿都应增加对空/仅空白内容的过滤。 - 若草稿与某个业务状态(如当前模板 ID)强关联,应确保两者一并持久化和恢复,避免状态不一致。 - 对兜底初始化逻辑(如默认模板加载)增加更严格的防护,防止被无效中间状态提前截断。 + +--- + +## 记录 2:关键帧一键插入占位符功能实现 + +**A. 具体问题** +用户希望视频分析面板中的关键帧截图除了拖拽插入外,还能通过点击 "插入" 按钮一键自动填充到编辑器中第一个空置的 `image-placeholder`。 + +**B. 产生问题原因** +原先仅支持拖拽方式将关键帧放入占位符。当关键帧数量多或占位符位置较远时,操作不便。且 `handleDrop` 中的填充逻辑未抽离,无法被其他交互方式复用。 + +**C. 解决问题方案** +1. 将 `handleDrop` 中的 HTML 填充逻辑抽离为 `fillPlaceholder(placeholder, frame)` 公共函数。 +2. 新增 `insertFrameToPlaceholder(frame)` 函数:通过 `editorRef.current.querySelector('.image-placeholder:not(.has-image)')` 查找第一个空置占位符,找到则调用 `fillPlaceholder`,未找到则 `alert('没有可插入图片的空位')`。 +3. 在关键帧卡片底部的 `timeFormatted` 与 "可拖拽" 之间新增 "插入" 按钮,使用 `opacity-0 group-hover:opacity-100 transition-opacity` 与 "可拖拽" 保持一致的显隐行为,并通过 `e.stopPropagation()` 避免触发卡片的视频跳转 `onClick`。 + +**D. 后续如何避免问题** +- 当同一交互效果(如填充占位符)需要支持多种触发方式(拖拽、按钮点击、快捷键等)时,应将核心逻辑抽离为独立函数,避免重复代码。 +- 在可点击子元素上务必注意事件冒泡控制,防止触发父级不必要的副作用(如此处的视频跳转)。 +- UI 提示文字(如 "插入"、"可拖拽")的显隐样式应尽量保持一致,减少用户认知成本。 diff --git a/工程分析/需求分析-2026-04-16-17-07-04.md b/工程分析/需求分析-2026-04-16-17-07-04.md new file mode 100644 index 0000000..f33fd3d --- /dev/null +++ b/工程分析/需求分析-2026-04-16-17-07-04.md @@ -0,0 +1,26 @@ +# 需求分析 — 2026-04-16-17-07-04 + +## 需求背景 +在 `report-editor` 页面的 **视频分析 - 关键帧摘取** 区域,目前用户需要通过鼠标拖拽的方式将截图插入到报告编辑器的 `image-placeholder` 中。当关键帧数量较多或占位符位置较远时,拖拽操作不够便捷,用户体验有待提升。 + +## 功能目标 +为每个关键帧截图增加一个 **"插入"** 按钮,实现一键自动插入到编辑器中第一个空置的 `image-placeholder` 中。 + +具体要求: +1. "插入" 按钮放置在每张截图底部的时间格式(如 `00:15`)与 "可拖拽" 提示文字之间。 +2. "插入" 按钮的显隐行为与右侧的 "可拖拽" 文字保持一致:仅当鼠标聚焦/悬停到该截图卡片上时才显示(`opacity-0 group-hover:opacity-100`)。 +3. 点击 "插入" 后,系统在当前编辑器内容中查找第一个尚未填充图片的 `.image-placeholder` 元素(即不含 `has-image` class 的占位符)。 +4. 找到后,将该关键帧的图片数据自动填充到该占位符中(效果等价于拖拽放下)。 +5. 填充后同步更新 `contentRef.current` 并保存草稿。 +6. 若编辑器中不存在空置的 `image-placeholder`,则给出提示(如 `alert('没有可插入图片的空位')`)。 + +## 涉及页面/模块 +- `src/pages/ReportEditor.tsx` —— 关键帧列表渲染区域 + 新增插入逻辑 + +## 验收标准 +- [ ] 每个关键帧截图底部出现 "插入" 按钮,位置在时间文字和 "可拖拽" 之间。 +- [ ] "插入" 按钮只在鼠标悬停到该截图卡片上时显示,移开即隐藏。 +- [ ] 点击 "插入" 后,第一个空置的 `image-placeholder` 被自动填充为该帧图片。 +- [ ] 若无可插入的空占位符,弹出友好提示。 +- [ ] 插入后编辑器内容、草稿状态正确同步。 +- [ ] `npm run lint` 零错误,`npm run build` 构建通过。