fix(editor): contentEditable回车导致段落溢出.ai-content
- handleAIGenerate中获取currentHtml前增加溢出段落合并逻辑 - 遍历.ai-content之后的兄弟<p>节点,移回.ai-content内 - 合并后同步更新contentRef和saveDraftToStorage - 确保diff弹窗左侧能显示AI可编辑区域内的全部段落
This commit is contained in:
@@ -883,6 +883,23 @@ export default function ReportEditor() {
|
||||
}
|
||||
}
|
||||
const targetRegionEl = editorRef.current?.querySelector(`.ai-region[data-ai-id="${actualTargetId}"] .ai-content`) as HTMLElement | null;
|
||||
// 合并溢出的段落:浏览器 contentEditable 可能在回车时把 <p> 生成到 .ai-content 之外
|
||||
const aiRegion = editorRef.current?.querySelector(`.ai-region[data-ai-id="${actualTargetId}"]`);
|
||||
if (aiRegion && targetRegionEl) {
|
||||
let nextSibling = targetRegionEl.nextElementSibling;
|
||||
while (nextSibling) {
|
||||
const toMove = nextSibling;
|
||||
nextSibling = nextSibling.nextElementSibling;
|
||||
if (toMove.tagName === 'P') {
|
||||
targetRegionEl.appendChild(toMove);
|
||||
}
|
||||
}
|
||||
// 同步更新 contentRef 和草稿
|
||||
if (editorRef.current) {
|
||||
contentRef.current = editorRef.current.innerHTML;
|
||||
saveDraftToStorage();
|
||||
}
|
||||
}
|
||||
const currentHtml = targetRegionEl ? targetRegionEl.innerHTML.replace(/​/g, '').trim() : '';
|
||||
const globalContextText = editorRef.current?.innerText || '';
|
||||
let messageContent: any;
|
||||
|
||||
46
工程分析/20260419_1805/实现方案.md
Normal file
46
工程分析/20260419_1805/实现方案.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# 实现方案
|
||||
|
||||
## 修改文件
|
||||
- `src/pages/ReportEditor.tsx`
|
||||
|
||||
## 修改位置:`handleAIGenerate` 函数内,获取 `currentHtml` 之前(约 line 885)
|
||||
|
||||
**原代码**:
|
||||
```tsx
|
||||
const targetRegionEl = editorRef.current?.querySelector(`.ai-region[data-ai-id="${actualTargetId}"] .ai-content`) as HTMLElement | null;
|
||||
const currentHtml = targetRegionEl ? targetRegionEl.innerHTML.replace(/​/g, '').trim() : '';
|
||||
```
|
||||
|
||||
**新代码**:
|
||||
```tsx
|
||||
const targetRegionEl = editorRef.current?.querySelector(`.ai-region[data-ai-id="${actualTargetId}"] .ai-content`) as HTMLElement | null;
|
||||
// 合并溢出的段落:浏览器 contentEditable 可能在回车时把 <p> 生成到 .ai-content 之外
|
||||
const aiRegion = editorRef.current?.querySelector(`.ai-region[data-ai-id="${actualTargetId}"]`);
|
||||
if (aiRegion && targetRegionEl) {
|
||||
let nextSibling = targetRegionEl.nextElementSibling;
|
||||
while (nextSibling) {
|
||||
const toMove = nextSibling;
|
||||
nextSibling = nextSibling.nextElementSibling;
|
||||
if (toMove.tagName === 'P') {
|
||||
targetRegionEl.appendChild(toMove);
|
||||
}
|
||||
}
|
||||
// 同步更新 contentRef 和草稿
|
||||
if (editorRef.current) {
|
||||
contentRef.current = editorRef.current.innerHTML;
|
||||
saveDraftToStorage();
|
||||
}
|
||||
}
|
||||
const currentHtml = targetRegionEl ? targetRegionEl.innerHTML.replace(/​/g, '').trim() : '';
|
||||
```
|
||||
|
||||
**变更点**:
|
||||
1. 新增 `aiRegion` 查询,获取 `.ai-region` 容器
|
||||
2. 遍历 `targetRegionEl.nextElementSibling`,把所有 `<p>` 标签移回 `targetRegionEl`
|
||||
3. 移动完成后同步更新 `contentRef.current` 和调用 `saveDraftToStorage()`
|
||||
4. 然后才获取 `currentHtml`
|
||||
|
||||
**为什么这样可行**:
|
||||
- `confirmAiInjection` 只替换 `.ai-content` 的 innerHTML
|
||||
- 修复后 `.ai-content` 已包含所有段落,注入时自然替换全部
|
||||
- 溢出的段落不会再留在 `.ai-region` 内造成重复
|
||||
45
工程分析/20260419_1805/测试方案.md
Normal file
45
工程分析/20260419_1805/测试方案.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# 测试方案
|
||||
|
||||
## 测试环境
|
||||
- 浏览器访问 `http://localhost:4173/`
|
||||
- 进入「图文报告生成」→ 新建报告
|
||||
|
||||
## 测试用例 1:溢出段落自动合并
|
||||
|
||||
**前置条件**:
|
||||
手动构造一个 DOM 结构被「破坏」的 AI 区域(模拟用户回车导致段落溢出):
|
||||
1. 插入 AI 可编辑区域「手术步骤」
|
||||
2. 在区域内输入第 2 段内容
|
||||
3. 在区域末尾按回车,输入第 3、4、5 段(观察 DOM,确认 `<p>` 标签变成了 `.ai-content` 的兄弟节点)
|
||||
|
||||
**步骤**:
|
||||
1. 勾选「允许修改正文」→ 选中「手术步骤」区域
|
||||
2. 发送「请完善手术步骤描述」
|
||||
|
||||
**预期结果**:
|
||||
- diff 弹窗左侧「原始版本」应显示全部 2-5 段内容,而不只是第 2 段
|
||||
- diff 弹窗右侧「AI 提议版本」也显示完整的多段落内容
|
||||
- 确认注入后,编辑器中 AI 区域的全部内容被替换,没有重复段落
|
||||
|
||||
## 测试用例 2:正常结构不受影响
|
||||
|
||||
**前置条件**:
|
||||
AI 可编辑区域内的所有段落都在 `.ai-content` 内部(正常结构)。
|
||||
|
||||
**步骤**:
|
||||
1. 发送修改指令
|
||||
|
||||
**预期结果**:
|
||||
- diff 弹窗左侧正常显示所有段落
|
||||
- 合并逻辑不会误删或误移任何内容
|
||||
|
||||
## 测试用例 3:编译与部署
|
||||
|
||||
**步骤**:
|
||||
1. 执行 `npm run build`
|
||||
2. 确认无 TypeScript 编译错误
|
||||
3. 预览服务正常启动并返回 200
|
||||
|
||||
**预期结果**:
|
||||
- `vite build` 成功完成
|
||||
- 预览页面可正常访问
|
||||
40
工程分析/20260419_1805/需求分析.md
Normal file
40
工程分析/20260419_1805/需求分析.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# 需求分析
|
||||
|
||||
## 时间戳
|
||||
2026-04-19 18:05
|
||||
|
||||
## 需求来源
|
||||
用户发现 AI 修改确认弹窗的「原始版本」左侧只显示了一段内容,而编辑器中的 AI 可编辑区域实际上有 2-5 段内容。
|
||||
|
||||
## 现象
|
||||
从用户提供的 DOM 源码可以清楚看到:
|
||||
|
||||
```html
|
||||
<div class="ai-content">
|
||||
<p><span>2.腹腔镜探查:...</span></p>
|
||||
</div>
|
||||
<p>3.切除胆囊:...</p>
|
||||
<p>4.检查腹腔内无活动性出血及漏胆后:...</p>
|
||||
<p>5.手术顺利,麻醉满意:...</p>
|
||||
```
|
||||
|
||||
- `.ai-content` 内只有第 2 段
|
||||
- 第 3、4、5 段变成了 `.ai-content` 的**兄弟节点**(在 `.ai-region` 内但在 `.ai-content` 外)
|
||||
- 左侧 diff 弹窗只显示 `.ai-content.innerHTML`,所以只有第 2 段
|
||||
|
||||
## 根因分析
|
||||
|
||||
**浏览器 contentEditable 机制的锅**:
|
||||
当用户在 `.ai-content`(一个 contentEditable 的 div)内按回车换行,或粘贴包含多个段落的外部文本时,浏览器的默认行为会截断当前的 `div`,在同级生成新的 `<p>` 标签。这导致后续的段落脱离了 `.ai-content` 父容器,变成了 `.ai-region` 的直接子节点。
|
||||
|
||||
## 解决方向
|
||||
|
||||
**代码层面修复**:在 `handleAIGenerate` 获取 `currentHtml` 之前,自动把 `.ai-region` 内 `.ai-content` 之外的 `<p>` 节点移回 `.ai-content`。这样:
|
||||
1. `currentHtml` 就能包含所有段落
|
||||
2. 左侧 diff 弹窗显示全部内容
|
||||
3. `confirmAiInjection` 注入时替换 `.ai-content`,此时 `.ai-content` 已包含所有段落
|
||||
|
||||
## 约束条件
|
||||
- 只移动 `.ai-content` 之后的 `<p>` 节点,不移动 `.ai-region-label` 或其他元素
|
||||
- 移动后要同步更新 `contentRef.current` 和 `saveDraftToStorage()`
|
||||
- 不改变现有 diff 弹窗和注入逻辑
|
||||
43
工程分析/经验记录.md
43
工程分析/经验记录.md
@@ -774,3 +774,46 @@ AI 修改确认弹窗右侧出现了不属于目标区域的内容:术后情
|
||||
**D. 后续如何避免问题**
|
||||
- 在向大模型发送局部修改请求时,**必须设置严格的内容边界(Fencing)**。全局上下文可以提供给 AI 作为背景理解,但必须在 Prompt 中明确声明"仅供理解,严禁输出"。
|
||||
- 避免使用"补充完善""基于全局信息扩展"等容易被大模型过度解读的措辞。大模型会尽其所能地"满足"用户的指令,即使这意味着越界生成。
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 记录 34:contentEditable 回车导致段落溢出 .ai-content
|
||||
|
||||
**A. 具体问题**
|
||||
AI 修改确认弹窗的「原始版本」左侧只显示了 AI 可编辑区域中的一段内容,但编辑器中该区域实际上有 2-5 段。从 DOM 源码可以看到:
|
||||
```html
|
||||
<div class="ai-content"><p>第2段</p></div>
|
||||
<p>第3段</p>
|
||||
<p>第4段</p>
|
||||
<p>第5段</p>
|
||||
```
|
||||
第 3-5 段变成了 `.ai-content` 的兄弟节点,不在 `.ai-content` 内部。
|
||||
|
||||
**B. 产生问题原因**
|
||||
浏览器原生 `contentEditable` 机制在用户按回车换行时,会截断当前的块级容器(`.ai-content` div),在同级生成新的 `<p>` 标签。这导致后续段落脱离了 `.ai-content` 父容器,变成了 `.ai-region` 的直接子节点。
|
||||
|
||||
**C. 解决问题方案**
|
||||
在 `handleAIGenerate` 获取 `currentHtml` 之前,增加溢出段落合并逻辑:
|
||||
```ts
|
||||
const aiRegion = editorRef.current?.querySelector(`.ai-region[data-ai-id="${actualTargetId}"]`);
|
||||
if (aiRegion && targetRegionEl) {
|
||||
let nextSibling = targetRegionEl.nextElementSibling;
|
||||
while (nextSibling) {
|
||||
const toMove = nextSibling;
|
||||
nextSibling = nextSibling.nextElementSibling;
|
||||
if (toMove.tagName === 'P') {
|
||||
targetRegionEl.appendChild(toMove);
|
||||
}
|
||||
}
|
||||
if (editorRef.current) {
|
||||
contentRef.current = editorRef.current.innerHTML;
|
||||
saveDraftToStorage();
|
||||
}
|
||||
}
|
||||
```
|
||||
遍历 `.ai-content` 之后的所有兄弟节点,把 `<p>` 标签移回 `.ai-content` 内,然后同步更新 contentRef 和草稿。
|
||||
|
||||
**D. 后续如何避免问题**
|
||||
- `contentEditable` 中的嵌套容器(如 `.ai-content`)在用户输入时极易被浏览器原生编辑行为破坏结构。任何依赖特定 DOM 层级关系的功能,都必须在读取数据前做**结构完整性检查和修复**。
|
||||
- 对于 AI 区域这类核心功能,应考虑在编辑器层面增加 `keydown`/`paste` 事件拦截,或改用更可控的编辑方案(如 ProseMirror/Slate)来替代原生 `contentEditable`。
|
||||
|
||||
Reference in New Issue
Block a user