From 90c9c7f2b0adeaf5d097ac2bd99141234a93c848 Mon Sep 17 00:00:00 2001 From: admin <572701190@qq.com> Date: Thu, 16 Apr 2026 17:07:04 +0800 Subject: [PATCH] backup before modification at 2026-04-16-17-07-04 --- src/pages/ReportEditor.tsx | 15 +++-- 工程分析/实现方案-2026-04-16-16-51-00.md | 69 +++++++++++++++++++++ 工程分析/测试方案-2026-04-16-16-51-00.md | 76 ++++++++++++++++++++++++ 工程分析/经验记录.md | 24 ++++++++ 工程分析/需求分析-2026-04-16-16-51-00.md | 36 +++++++++++ 5 files changed, 215 insertions(+), 5 deletions(-) create mode 100644 工程分析/实现方案-2026-04-16-16-51-00.md create mode 100644 工程分析/测试方案-2026-04-16-16-51-00.md create mode 100644 工程分析/经验记录.md create mode 100644 工程分析/需求分析-2026-04-16-16-51-00.md diff --git a/src/pages/ReportEditor.tsx b/src/pages/ReportEditor.tsx index 1694dbe..3a4306b 100644 --- a/src/pages/ReportEditor.tsx +++ b/src/pages/ReportEditor.tsx @@ -88,11 +88,12 @@ export default function ReportEditor() { if (key) { storage.set(key, { content: contentRef.current, + loadedTemplateId, draftReportId: reportId || null, ...stateRef.current }); } - }, [reportId]); + }, [reportId, loadedTemplateId]); useEffect(() => { const user = storage.get('currentUser', null); @@ -122,10 +123,11 @@ export default function ReportEditor() { setCapturedFrames(draft.capturedFrames.sort((a: CapturedFrame, b: CapturedFrame) => a.time - b.time)); } if (draft.activeTab) setActiveTab(draft.activeTab); - if (editorRef.current && typeof draft.content === 'string') { + if (editorRef.current && typeof draft.content === 'string' && draft.content.trim().length > 0) { editorRef.current.innerHTML = draft.content; contentRef.current = draft.content; contentLoadedRef.current = true; + setLoadedTemplateId(draft.loadedTemplateId || ''); setTimeout(() => updatePageHeight(), 0); } } else { @@ -169,10 +171,11 @@ export default function ReportEditor() { setCapturedFrames(draft.capturedFrames.sort((a: CapturedFrame, b: CapturedFrame) => a.time - b.time)); } if (draft.activeTab) setActiveTab(draft.activeTab); - if (editorRef.current && typeof draft.content === 'string') { + if (editorRef.current && typeof draft.content === 'string' && draft.content.trim().length > 0) { editorRef.current.innerHTML = draft.content; contentRef.current = draft.content; contentLoadedRef.current = true; + setLoadedTemplateId(draft.loadedTemplateId || ''); setTimeout(() => updatePageHeight(), 0); } } @@ -616,10 +619,11 @@ export default function ReportEditor() { const draft = key ? storage.get | null>(key, null) : null; if (reportId) { - if (draft && draft.draftReportId === reportId && typeof draft.content === 'string') { + if (draft && draft.draftReportId === reportId && typeof draft.content === 'string' && draft.content.trim().length > 0) { editorRef.current.innerHTML = draft.content; contentRef.current = draft.content; contentLoadedRef.current = true; + setLoadedTemplateId(draft.loadedTemplateId || ''); setTimeout(() => updatePageHeight(), 0); return; } @@ -638,10 +642,11 @@ export default function ReportEditor() { return; } } else { - if (draft && !draft.draftReportId && typeof draft.content === 'string') { + if (draft && !draft.draftReportId && typeof draft.content === 'string' && draft.content.trim().length > 0) { editorRef.current.innerHTML = draft.content; contentRef.current = draft.content; contentLoadedRef.current = true; + setLoadedTemplateId(draft.loadedTemplateId || ''); setTimeout(() => updatePageHeight(), 0); return; } diff --git a/工程分析/实现方案-2026-04-16-16-51-00.md b/工程分析/实现方案-2026-04-16-16-51-00.md new file mode 100644 index 0000000..60d0315 --- /dev/null +++ b/工程分析/实现方案-2026-04-16-16-51-00.md @@ -0,0 +1,69 @@ +# 实现方案 — 2026-04-16-16-51-00 + +## 技术思路 +本次修复聚焦于 `ReportEditor.tsx` 中草稿(draft)恢复与默认模板加载的竞争条件问题。核心思路是: +1. **拒绝加载空白草稿**:将空字符串(或仅空白字符)的 draft 视为无效,不拦截默认模板加载流程。 +2. **草稿中携带模板信息**:在自动保存 draft 时追加 `loadedTemplateId`,恢复草稿时同步还原,确保模板选择器显示正确。 +3. **统一初始化路径**:`useEffect` 与 `useLayoutEffect` 中的草稿/默认模板判断逻辑保持一致,避免其中一个抢先设置空白内容。 + +--- + +## 修改文件清单 + +| 文件 | 变更类型 | 说明 | +|------|----------|------| +| `src/pages/ReportEditor.tsx` | 修改 | 修复草稿恢复条件、保存/恢复 `loadedTemplateId`、统一空白判断逻辑 | + +--- + +## 关键代码变更说明 + +### 1. 保存 draft 时追加 `loadedTemplateId` + +在 `saveDraftToStorage` 回调中,将当前 `loadedTemplateId` 一并持久化: + +```tsx +storage.set(key, { + content: contentRef.current, + loadedTemplateId, // 新增 + draftReportId: reportId || null, + ...stateRef.current +}); +``` + +### 2. 恢复 draft 时同步恢复 `loadedTemplateId`(useEffect 初始化逻辑) + +**新建报告分支(无 `reportId`)** +- 将条件 `typeof draft.content === 'string'` 改为 `typeof draft.content === 'string' && draft.content.trim().length > 0`。 +- 若草稿有效,除了回填内容,还要执行 `setLoadedTemplateId(draft.loadedTemplateId || '')`。 + +**编辑报告分支(有 `reportId`)** +- 同样使用 `draft.content.trim().length > 0` 判断,避免空白草稿覆盖已有报告内容。 +- 恢复 `loadedTemplateId`(虽然在编辑模式下模板选择器通常不显示具体模板名,但保持一致性)。 + +### 3. useLayoutEffect 安全网逻辑同步修复 + +`useLayoutEffect`(第 611 行起)作为 editor ref 就绪后的二次安全网,其草稿判断条件也要同步修改: +- `typeof draft.content === 'string'` → `typeof draft.content === 'string' && draft.content.trim().length > 0` +- 恢复 `loadedTemplateId` + +### 4. 默认模板加载时同步设置 `loadedTemplateId` + +在加载 `settings.defaultTemplate` 对应模板内容时,当前代码已经设置了 `setLoadedTemplateId(tpl.id)`,无需改动。 +在兜底使用 `defaultReportContent` 时,`loadedTemplateId` 保持为空字符串(显示"无"),这符合语义,因为兜底内容不是用户选中的模板。 + +--- + +## 风险点及应对策略 + +| 风险 | 影响 | 应对策略 | +|------|------|----------| +| 修改了多处 draft 判断逻辑,可能漏改 | 某些路由切换场景仍出现空白 | 在 `useEffect` 和 `useLayoutEffect` 两处共 **4 个草稿加载点** 统一替换判断条件 | +| `loadedTemplateId` 加入 draft 后,旧 draft 兼容性 | 旧 draft 没有该字段,恢复时值为 `undefined`,模板选择器短暂显示"无" | 使用 `draft.loadedTemplateId || ''` 兜底,不影响功能 | +| 用户之前确实清空了内容再离开 | 会被视为无效草稿而丢失空白状态 | 这是预期行为:空白内容等价于未开始编辑,应回退到默认模板 | + +--- + +## 改动范围总结 +- 仅修改 `src/pages/ReportEditor.tsx`,不触及路由、存储封装或其他页面逻辑。 +- 不引入新依赖。 diff --git a/工程分析/测试方案-2026-04-16-16-51-00.md b/工程分析/测试方案-2026-04-16-16-51-00.md new file mode 100644 index 0000000..a13fc5b --- /dev/null +++ b/工程分析/测试方案-2026-04-16-16-51-00.md @@ -0,0 +1,76 @@ +# 测试方案 — 2026-04-16-16-51-00 + +## 测试环境准备 +1. 确保项目依赖已安装:`npm install` 已完成。 +2. 使用测试账号 `admin / 123456`(超级管理员)登录系统。 +3. 在 **系统设置** 页面中,确认 **图文报告生成默认模板** 已设置为 **"腹腔镜胆囊切除术报告"**。 + +--- + +## 测试项清单 + +### 测试项 1:新建报告时正确加载默认模板 +**测试步骤**: +1. 登录后,点击左侧菜单 **"图文报告生成"**(`/report-editor`,无 `id` 参数)。 +2. 观察页面顶部 **"当前模板(及重置模板):"** 下拉框显示内容。 +3. 观察中间编辑区域(`editor-content-wrapper print-wrapper`)是否有模板内容。 + +**预期结果**: +- 顶部模板选择器显示 **"腹腔镜胆囊切除术报告"**(或用户设置的默认模板名称),而非"无"。 +- 编辑区域显示该模板的完整 HTML 内容(包含标题、表格、图片占位符等),不是纯白色空白。 + +--- + +### 测试项 2:从其他页面返回后未编辑不显示空白 +**测试步骤**: +1. 在 **工作台**(`/dashboard`)页面停留。 +2. 通过左侧菜单再次进入 **"图文报告生成"**。 +3. 不要输入任何内容,直接再切回 **工作台**,然后再切回 **"图文报告生成"**。 + +**预期结果**: +- 每次进入 `/report-editor`,编辑区域都应正确显示默认模板内容。 +- 不会出现白色空白页面。 + +--- + +### 测试项 3:编辑已有报告时不被空白草稿覆盖 +**测试步骤**: +1. 进入 **报告管理**,打开一份已有内容的报告进行编辑(URL 带有 `?id=xxx`)。 +2. 不要做任何修改,直接刷新浏览器页面。 + +**预期结果**: +- 页面加载后显示该报告原有的完整内容,不会被空白草稿覆盖为默认模板。 + +--- + +### 测试项 4:有效草稿恢复后模板选择器显示正确 +**测试步骤**: +1. 进入 **"图文报告生成"**,确认已加载默认模板。 +2. 在编辑器中随意输入几个字(确保内容非空)。 +3. 切到 **工作台**,再切回 **"图文报告生成"**。 + +**预期结果**: +- 编辑器恢复刚才输入的内容。 +- 顶部模板选择器仍显示 **"腹腔镜胆囊切除术报告"**(因为草稿中已保存 `loadedTemplateId`)。 + +--- + +### 测试项 5:构建与类型检查回归 +**测试步骤**: +1. 在项目根目录执行: + ```bash + npm run lint + npm run build + ``` + +**预期结果**: +- `npm run lint` 无 TypeScript 编译错误。 +- `npm run build` 构建成功,生成 `dist/` 目录。 + +--- + +## 回归验证范围 +- [ ] `SystemSettings.tsx` 未被修改,默认模板设置功能保持正常。 +- [ ] `storage.ts` 未被修改,localStorage 读写保持正常。 +- [ ] 报告保存(草稿/完成)功能未被破坏。 +- [ ] 视频分析面板与编辑器交互保持正常。 diff --git a/工程分析/经验记录.md b/工程分析/经验记录.md new file mode 100644 index 0000000..5038a8c --- /dev/null +++ b/工程分析/经验记录.md @@ -0,0 +1,24 @@ +# 经验记录 + +--- + +## 记录 1:report-editor 新建报告时显示空白模板 + +**A. 具体问题** +超级管理员进入 `/report-editor`(新建报告)时,编辑区域为纯白色空白,顶部模板选择器显示"无",但 system-settings 中已配置了默认模板。 + +**B. 产生问题原因** +1. `ReportEditor.tsx` 在组件卸载(如页面切换)时会自动将当前编辑器内容保存为草稿(draft)。即使用户未输入任何内容,保存的 `content` 也是空字符串 `""`。 +2. 初始化 effect 中判断草稿是否有效的条件仅使用了 `typeof draft.content === 'string'`,空字符串满足该条件,导致编辑器被填充为空白 HTML,并将 `contentLoadedRef.current` 设为 `true`。 +3. 由于 `contentLoadedRef.current` 已被置为 `true`,后续加载 `settings.defaultTemplate` 的默认模板分支被完全跳过,从而永远显示空白。 +4. 此外,草稿中未保存 `loadedTemplateId`,即使内容非空时恢复草稿,模板选择器也会因缺少状态而显示"无"。 + +**C. 解决问题方案** +1. 在 `saveDraftToStorage` 中将当前 `loadedTemplateId` 一并存入 draft。 +2. 将四处草稿恢复的判断条件从 `typeof draft.content === 'string'` 收紧为 `typeof draft.content === 'string' && draft.content.trim().length > 0`,使空白草稿不再拦截默认模板加载。 +3. 恢复草稿时同步执行 `setLoadedTemplateId(draft.loadedTemplateId || '')`,确保模板选择器名称正确。 + +**D. 后续如何避免问题** +- 在前端使用 contentEditable 的自动保存机制时,保存和恢复草稿都应增加对空/仅空白内容的过滤。 +- 若草稿与某个业务状态(如当前模板 ID)强关联,应确保两者一并持久化和恢复,避免状态不一致。 +- 对兜底初始化逻辑(如默认模板加载)增加更严格的防护,防止被无效中间状态提前截断。 diff --git a/工程分析/需求分析-2026-04-16-16-51-00.md b/工程分析/需求分析-2026-04-16-16-51-00.md new file mode 100644 index 0000000..7a03543 --- /dev/null +++ b/工程分析/需求分析-2026-04-16-16-51-00.md @@ -0,0 +1,36 @@ +# 需求分析 — 2026-04-16-16-51-00 + +## 需求背景 +用户在进入 **report-editor**(图文报告生成)页面时,期望编辑器能够自动加载在 **system-settings** 中配置的"图文报告生成默认模板"。但目前存在以下异常现象: + +1. 超级管理员进入 report-editor 时,顶部模板选择器显示 **"无"**,编辑区域(`editor-content-wrapper print-wrapper`)为纯白色空白,没有加载任何模板内容。 +2. 在 system-settings 中已正确设置默认模板为 **"腹腔镜胆囊切除术报告"**,但 report-editor 未按预期加载。 +3. 用户从 `/dashboard` 等其他页面返回 `/report-editor` 后,若之前未进行过有效编辑,也可能出现白色空白模板的情况。 + +## 功能目标 +修复 report-editor 页面初始化时的模板加载逻辑,确保: +- 新建报告(无 `reportId`)时,优先加载系统设置中的默认模板。 +- 只有当用户确实在编辑器中有过有效编辑内容时,才从本地草稿(draft)恢复。 +- 模板选择器中的"当前模板(及重置模板):"应正确反映当前加载的模板名称,而不是显示"无"。 +- 从其他页面返回或重新进入 report-editor 时,未编辑状态下不应出现空白模板。 + +## 涉及页面/模块 +- `src/pages/ReportEditor.tsx` —— 核心问题所在,草稿恢复与默认模板加载逻辑 +- `src/pages/SystemSettings.tsx` —— 默认模板设置页面(验证配置读取逻辑) +- `src/utils/storage.ts` —— localStorage 读写封装(辅助确认) + +## 问题根因(预分析) +通过代码审阅,发现以下导致空白的根因: + +1. **空字符串草稿被当作有效内容加载**:`saveDraftToStorage` 在组件卸载时自动保存草稿。如果用户未在编辑器中输入任何内容,保存的 `content` 会是空字符串 `""`。在初始化 effect 中,判断条件 `typeof draft.content === 'string'` 对空字符串也返回 `true`,导致编辑器被填充为空白 HTML,跳过了后续默认模板加载逻辑。 + +2. **草稿中未记录模板 ID**:`loadedTemplateId` 没有被存入 draft。当从 draft 恢复时,即使内容非空,模板选择器也因缺少 `loadedTemplateId` 而显示"无"。 + +3. **默认模板加载逻辑被空白草稿截断**:由于空草稿提前将 `contentLoadedRef.current` 设为 `true`,真正的默认模板分支 (`settings.defaultTemplate`) 永远不会执行。 + +## 验收标准 +- [ ] 超级管理员进入 report-editor 时,编辑区域正确显示 system-settings 中设置的默认模板内容。 +- [ ] 模板选择器显示当前已加载的默认模板名称,而非"无"。 +- [ ] 从 dashboard 等其他页面返回 report-editor,未编辑情况下不显示空白模板。 +- [ ] `npm run lint` 类型检查零错误。 +- [ ] `npm run build` 构建通过。