fix(editor): AI注入后Ctrl+Z失效 + 字体格式统一

- confirmAiInjection改用Range.selectNodeContents + execCommand('insertHTML')保留浏览器撤销栈
- handleAIGenerate中对cleanHtml增加<p>标签内联样式注入:padding 0px、font-family SimSun、font-size 12pt、line-height 1.5
- 确保AI替换后的文字字体与原有文字完全一致
This commit is contained in:
2026-04-19 20:33:43 +08:00
parent b24ba08658
commit 7275906f3c
5 changed files with 208 additions and 1 deletions

View File

@@ -957,6 +957,7 @@ export default function ReportEditor() {
cleanHtml = cleanHtml.replace(/<br\s*\/?>/gi, '');
cleanHtml = cleanHtml.replace(/<\/p>\s*<p>/gi, '</p><p>');
cleanHtml = cleanHtml.trim();
cleanHtml = cleanHtml.replace(/<p>/gi, '<p style="padding: 0px; font-family: SimSun; font-size: 12pt; line-height: 1.5;">');
if (targetRegionEl) {
setDiffModal({
isOpen: true,
@@ -982,7 +983,13 @@ export default function ReportEditor() {
if (!editorRef.current) return;
const targetContent = editorRef.current.querySelector(`.ai-region[data-ai-id="${regionId}"] .ai-content`) as HTMLElement;
if (targetContent) {
targetContent.innerHTML = newHtml;
targetContent.focus();
const sel = window.getSelection();
const range = document.createRange();
range.selectNodeContents(targetContent);
sel?.removeAllRanges();
sel?.addRange(range);
document.execCommand('insertHTML', false, newHtml);
targetContent.style.transition = 'background-color 0.3s ease';
targetContent.style.backgroundColor = '#bfdbfe';
setTimeout(() => {

View File

@@ -0,0 +1,94 @@
# 实现方案
## 修改文件
- `src/pages/ReportEditor.tsx`
## 修改 1`confirmAiInjection` 保留撤销栈(约 line 981-998
**原代码**
```tsx
const confirmAiInjection = (newHtml: string, regionId: string) => {
if (!editorRef.current) return;
const targetContent = editorRef.current.querySelector(`.ai-region[data-ai-id="${regionId}"] .ai-content`) as HTMLElement;
if (targetContent) {
targetContent.innerHTML = newHtml;
targetContent.style.transition = 'background-color 0.3s ease';
targetContent.style.backgroundColor = '#bfdbfe';
setTimeout(() => {
targetContent.style.backgroundColor = '#eff6ff';
setTimeout(() => {
targetContent.style.backgroundColor = 'transparent';
}, 800);
}, 400);
contentRef.current = editorRef.current.innerHTML;
saveDraftToStorage();
}
setDiffModal(null);
};
```
**新代码**
```tsx
const confirmAiInjection = (newHtml: string, regionId: string) => {
if (!editorRef.current) return;
const targetContent = editorRef.current.querySelector(`.ai-region[data-ai-id="${regionId}"] .ai-content`) as HTMLElement;
if (targetContent) {
targetContent.focus();
const sel = window.getSelection();
const range = document.createRange();
range.selectNodeContents(targetContent);
sel?.removeAllRanges();
sel?.addRange(range);
document.execCommand('insertHTML', false, newHtml);
targetContent.style.transition = 'background-color 0.3s ease';
targetContent.style.backgroundColor = '#bfdbfe';
setTimeout(() => {
targetContent.style.backgroundColor = '#eff6ff';
setTimeout(() => {
targetContent.style.backgroundColor = 'transparent';
}, 800);
}, 400);
contentRef.current = editorRef.current.innerHTML;
saveDraftToStorage();
}
setDiffModal(null);
};
```
**变更点**
1. 去掉 `targetContent.innerHTML = newHtml;`
2. 增加 `targetContent.focus()`
3. 使用 `Range.selectNodeContents(targetContent)` 选中区域内所有旧内容
4. 使用 `document.execCommand('insertHTML', false, newHtml)` 执行替换
5. 浏览器撤销栈会记录这次替换Ctrl+Z 可正常撤销
## 修改 2`handleAIGenerate` 中 `<p>` 标签注入样式(约 line 955-970
**原代码**
```tsx
if (responseJson.updatedHtml && aiModifyEnabled) {
let cleanHtml = responseJson.updatedHtml;
cleanHtml = cleanHtml.replace(/<br\s*\/?>/gi, '');
cleanHtml = cleanHtml.replace(/<\/p>\s*<p>/gi, '</p><p>');
cleanHtml = cleanHtml.trim();
if (targetRegionEl) {
```
**新代码**
```tsx
if (responseJson.updatedHtml && aiModifyEnabled) {
let cleanHtml = responseJson.updatedHtml;
cleanHtml = cleanHtml.replace(/<br\s*\/?>/gi, '');
cleanHtml = cleanHtml.replace(/<\/p>\s*<p>/gi, '</p><p>');
cleanHtml = cleanHtml.trim();
cleanHtml = cleanHtml.replace(/<p>/gi, '<p style="padding: 0px; font-family: SimSun; font-size: 12pt; line-height: 1.5;">');
if (targetRegionEl) {
```
**变更点**:在 `cleanHtml.trim()` 后增加一行正则替换,将所有 `<p>` 标签替换为带标准内联样式的 `<p>` 标签。
**样式值说明**
- `padding: 0px`:与原始段落一致
- `font-family: SimSun`:宋体
- `font-size: 12pt`12pt 字号
- `line-height: 1.5`1.5 倍行高

View File

@@ -0,0 +1,42 @@
# 测试方案
## 测试环境
- 浏览器访问 `http://localhost:4173/`
- 进入「图文报告生成」→ 新建报告
## 测试用例 1Ctrl+Z 可撤销 AI 修改
**步骤**
1. 编辑器中插入 AI 可编辑区域,写入一些内容
2. 勾选「允许修改正文」→ 发送修改指令
3. 在 diff 弹窗中点击「确认并写入报告」
4. 按 Ctrl+Z或点击工具栏撤销按钮
**预期结果**
- AI 修改的内容被撤销,恢复到修改前的状态
- 可连续按 Ctrl+Z 继续撤销更早的操作
- 撤销后内容完整,无 DOM 结构损坏
## 测试用例 2替换后字体格式一致
**步骤**
1. 编辑器中 AI 可编辑区域内原有内容带宋体 12pt 样式
2. 发送 AI 修改指令
3. 观察 diff 弹窗左右两侧
4. 确认注入后观察编辑器中该区域内容
**预期结果**
- diff 弹窗右侧「AI 提议版本」的字体为宋体 12pt与左侧一致
- 确认注入后,编辑器中 AI 区域的文字字体与周边/原有文字一致
- 无视觉割裂感
## 测试用例 3编译与部署
**步骤**
1. 执行 `npm run build`
2. 确认无 TypeScript 编译错误
3. 预览服务正常启动并返回 200
**预期结果**
- `vite build` 成功完成
- 预览页面可正常访问

View File

@@ -0,0 +1,29 @@
# 需求分析
## 时间戳
2026-04-19 20:30
## 需求来源
用户在 AI 修改确认后遇到两个问题:
1. 点击「确认并写入报告」后Ctrl+Z 撤销按钮无法撤销 AI 的修改
2. AI 替换后的文字字体格式与原有文字不一致(原有宋体 12pt替换后变为浏览器默认字体
## 问题 1Ctrl+Z 撤销失效
**现象**AI 修改确认注入后,按 Ctrl+Z 无法撤销。
**根因分析**
`confirmAiInjection` 使用 `targetContent.innerHTML = newHtml;` 直接修改 DOM 属性。这种方式绕过了浏览器 `contentEditable` 的原生撤销/重做历史栈,导致浏览器无法追踪这次更改。
## 问题 2字体格式不一致
**现象**
- 替换前:`<p style="padding: 0px; font-family: SimSun; font-size: 12pt; line-height: 1.5;">内容</p>`
- 替换后:`<p>内容</p>`(无内联样式,显示为浏览器默认字体)
**根因分析**
大模型严格按照指令返回纯净的 `<p>` 标签,没有内联样式。注入后浏览器使用默认字体渲染,与原有宋体 12pt 不一致。
## 解决方向
1. `confirmAiInjection` 改用 `Range.selectNodeContents` + `document.execCommand('insertHTML')`,让浏览器原生撤销栈记录这次替换
2. `handleAIGenerate` 中对 `cleanHtml` 进行正则替换,给 `<p>` 标签注入标准内联样式

View File

@@ -845,3 +845,38 @@ if (aiRegion && targetRegionEl) {
**D. 后续如何避免问题**
- 任何通过 iframe 或独立文档实现的打印/导出功能,都必须在 iframe 的 `<style>` 中独立维护打印样式,不能依赖外部 CSS 文件(因为外部样式不会自动注入 iframe
- 对于 diff 对比类 UI左右两侧容器应显式设置相同的默认字体样式避免依赖内容自带的内联样式造成视觉不一致。
---
## 记录 36AI 注入后 Ctrl+Z 失效 + 字体格式丢失
**A. 具体问题**
1. 点击「确认并写入报告」后Ctrl+Z 无法撤销 AI 的修改。
2. AI 替换后的文字丢失了原有内联样式(宋体 12pt显示为浏览器默认字体。
**B. 产生问题原因**
1. **撤销失效**`confirmAiInjection` 使用 `targetContent.innerHTML = newHtml;` 直接修改 DOM 属性。这种方式完全绕过了浏览器 `contentEditable` 的原生撤销/重做历史栈。
2. **字体丢失**:大模型返回的是纯净的 `<p>` 标签(如 `<p>内容</p>`),没有内联样式。替换后浏览器使用默认字体渲染,与原有 `<p style="font-family: SimSun; font-size: 12pt;">` 不一致。
**C. 解决问题方案**
1. **保留撤销栈**:将 `innerHTML = newHtml` 替换为:
```ts
targetContent.focus();
const sel = window.getSelection();
const range = document.createRange();
range.selectNodeContents(targetContent);
sel?.removeAllRanges();
sel?.addRange(range);
document.execCommand('insertHTML', false, newHtml);
```
`Range.selectNodeContents` 选中区域内所有旧内容,`execCommand('insertHTML')` 让浏览器原生撤销栈记录这次替换。
2. **注入内联样式**:在 `handleAIGenerate` 的 `cleanHtml` 清洗后增加:
```ts
cleanHtml = cleanHtml.replace(/<p>/gi, '<p style="padding: 0px; font-family: SimSun; font-size: 12pt; line-height: 1.5;">');
```
给所有 `<p>` 标签注入标准内联样式,确保替换后字体与原有文字一致。
**D. 后续如何避免问题**
- 在 `contentEditable` 环境中修改内容时,**优先使用 `Range.selectNodeContents` + `execCommand('insertHTML')` 而非直接 `innerHTML` 赋值**,前者能让浏览器原生撤销/重做栈正常工作。
- 当大模型返回的 HTML 缺少必要的内联样式时,应在**前端后处理阶段**统一注入样式,而不是依赖大模型生成完整的样式代码(大模型对样式生成的稳定性较差)。