backup before modification at 2026-04-16-17-15-37

This commit is contained in:
2026-04-16 17:15:37 +08:00
parent 90c9c7f2b0
commit a07e6e4e98
5 changed files with 269 additions and 8 deletions

View File

@@ -507,21 +507,38 @@ export default function ReportEditor() {
e.dataTransfer.setData('frameId', frame.id.toString());
};
const fillPlaceholder = (placeholder: HTMLElement, frame: CapturedFrame) => {
placeholder.innerHTML = `
<span class="delete-btn" contenteditable="false">×</span>
<img src="${frame.dataUrl}" style="max-width: 100%; height: auto; display: block; margin: 0 auto;" draggable="false">
`;
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 = `
<span class="delete-btn" contenteditable="false">×</span>
<img src="${frame.dataUrl}" style="max-width: 100%; height: auto; display: block; margin: 0 auto;" />
`;
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() {
</div>
<div className="text-[9px] font-bold text-text-muted mt-1.5 px-1 flex justify-between items-center">
<span>{frame.timeFormatted}</span>
<span className="text-accent opacity-0 group-hover:opacity-100 transition-opacity"></span>
<div className="flex items-center gap-2">
<button
onClick={(e) => { e.stopPropagation(); insertFrameToPlaceholder(frame); }}
className="text-accent opacity-0 group-hover:opacity-100 transition-opacity hover:underline"
>
</button>
<span className="text-accent opacity-0 group-hover:opacity-100 transition-opacity"></span>
</div>
</div>
<button
onClick={(e) => { e.stopPropagation(); setCapturedFrames(prev => prev.filter(f => f.id !== frame.id).sort((a, b) => a.time - b.time)); saveDraftToStorage(); }}

View File

@@ -0,0 +1,102 @@
# 实现方案 — 2026-04-16-17-07-04
## 技术思路
`ReportEditor.tsx` 中为关键帧卡片新增一个 **"插入"** 按钮,点击后自动将当前帧图片填充到编辑器中第一个空置的 `.image-placeholder` 中。
为保持代码整洁,将现有 `handleDrop` 中的占位符填充逻辑抽取为可复用的 `fillPlaceholder` 函数,供拖拽放下和按钮点击共同调用。
---
## 修改文件清单
| 文件 | 变更类型 | 说明 |
|------|----------|------|
| `src/pages/ReportEditor.tsx` | 修改 | 新增 `fillPlaceholder` 函数、新增 `insertFrameToPlaceholder` 函数、修改关键帧卡片 JSX |
---
## 关键代码变更说明
### 1. 抽取公共填充函数 `fillPlaceholder`
`handleDrop` 中的图片填充逻辑抽离:
```tsx
const fillPlaceholder = (placeholder: HTMLElement, frame: CapturedFrame) => {
placeholder.innerHTML = `
<span class="delete-btn" contenteditable="false">×</span>
<img src="${frame.dataUrl}" style="max-width: 100%; height: auto; display: block; margin: 0 auto;" draggable="false">
`;
placeholder.classList.add('has-image');
if (editorRef.current) contentRef.current = editorRef.current.innerHTML;
saveDraftToStorage();
};
```
并同步简化 `handleDrop`
```tsx
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) {
fillPlaceholder(placeholder, frame);
}
};
```
### 2. 新增一键插入函数 `insertFrameToPlaceholder`
```tsx
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);
};
```
### 3. 在关键帧卡片底部添加 "插入" 按钮
修改现有 JSX约第 1303 行附近),在 `timeFormatted` 与 "可拖拽" 之间插入按钮:
```tsx
<div className="text-[9px] font-bold text-text-muted mt-1.5 px-1 flex justify-between items-center">
<span>{frame.timeFormatted}</span>
<div className="flex items-center gap-2">
<button
onClick={(e) => { e.stopPropagation(); insertFrameToPlaceholder(frame); }}
className="text-accent opacity-0 group-hover:opacity-100 transition-opacity hover:underline"
>
</button>
<span className="text-accent opacity-0 group-hover:opacity-100 transition-opacity"></span>
</div>
</div>
```
按钮使用 `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`,不触及其他文件。
- 不引入新依赖。

View File

@@ -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` 未被修改,存储读写保持正常。
- [ ] 编辑器打印、保存草稿、完成报告功能保持正常。
- [ ] 关键帧的 "手动截取"、删除功能保持正常。

View File

@@ -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 提示文字(如 "插入"、"可拖拽")的显隐样式应尽量保持一致,减少用户认知成本。

View File

@@ -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` 构建通过。