backup before modification at 2026-04-16-17-15-37
This commit is contained in:
@@ -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(); }}
|
||||
|
||||
102
工程分析/实现方案-2026-04-16-17-07-04.md
Normal file
102
工程分析/实现方案-2026-04-16-17-07-04.md
Normal 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`,不触及其他文件。
|
||||
- 不引入新依赖。
|
||||
88
工程分析/测试方案-2026-04-16-17-07-04.md
Normal file
88
工程分析/测试方案-2026-04-16-17-07-04.md
Normal 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` 未被修改,存储读写保持正常。
|
||||
- [ ] 编辑器打印、保存草稿、完成报告功能保持正常。
|
||||
- [ ] 关键帧的 "手动截取"、删除功能保持正常。
|
||||
20
工程分析/经验记录.md
20
工程分析/经验记录.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 提示文字(如 "插入"、"可拖拽")的显隐样式应尽量保持一致,减少用户认知成本。
|
||||
|
||||
26
工程分析/需求分析-2026-04-16-17-07-04.md
Normal file
26
工程分析/需求分析-2026-04-16-17-07-04.md
Normal 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` 构建通过。
|
||||
Reference in New Issue
Block a user