# Bug修复: 跨子块批注状态污染 > **修复日期**: 2025-11-12 > **严重程度**: 🔴 高(影响批注核心功能) > **影响范围**: 批注和高亮系统 > **修复文件**: `js/annotations/annotation_logic.js` --- ## 🐛 Bug 描述 ### 问题表现 用户在详情页进行批注操作时: 1. 先做一次**跨子块高亮**(比如选中 8 个子块) 2. 再做**单子块内高亮**(比如只选中 109.0 内的文本) 3. **Bug**: 第 2 次操作却执行了跨 8 个子块的高亮 ❌ ### 用户日志 ``` [跨子块检测] 选择在同一个子块内 ✅ 检测正确 [跨子块检测] 未检测到跨子块选择,继续单子块处理 ✅ 流程正确 // 但是点击高亮按钮后: [跨子块操作] 执行操作: highlight-block, 涉及 8 个子块 ❌ 使用了旧数据! ``` --- ## 🔍 根源分析 ### 问题代码 **文件**: `annotation_logic.js:804-806` ```javascript const isCrossBlockOperation = annotationContextMenuElement.dataset.contextIsCrossBlock === "true"; if (isCrossBlockOperation) { return handleCrossBlockMenuAction(action, color, event); } ``` ### Bug 机制 #### 跨子块操作时(第1次) ```javascript // annotation_logic.js:1694-1699 annotationContextMenuElement.dataset.contextIsCrossBlock = "true"; annotationContextMenuElement.dataset.contextAffectedSubBlocks = JSON.stringify([...8个子块ID]); ``` ✅ 正确设置 #### 单子块操作时(第2次) ```javascript // annotation_logic.js:716-738 (修复前) annotationContextMenuElement.dataset.contextContentIdentifier = ...; annotationContextMenuElement.dataset.contextTargetIdentifier = ...; // ... 设置很多属性 // ❌ 问题:没有清除跨子块相关属性! // contextIsCrossBlock 仍然是 "true" // contextAffectedSubBlocks 仍然是旧的 8 个子块 ``` #### 点击菜单时 ```javascript // annotation_logic.js:804 const isCrossBlockOperation = dataset.contextIsCrossBlock === "true"; // ❌ 读到旧值 "true" if (isCrossBlockOperation) { return handleCrossBlockMenuAction(...); // ❌ 误调用跨子块处理 // 使用了旧的 contextAffectedSubBlocks(8个子块) } ``` ### 时序图 ``` 时间线: ┌─────────────────────────────────────────────────┐ │ 第 1 次操作:跨子块高亮(8 个子块) │ ├─────────────────────────────────────────────────┤ │ contextIsCrossBlock = "true" ✅ │ │ contextAffectedSubBlocks = [8个ID] ✅ │ └─────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────┐ │ 第 2 次操作:单子块高亮(109.0 内部分文本) │ ├─────────────────────────────────────────────────┤ │ detectCrossBlockSelection() → false ✅ │ │ 设置 contextContentIdentifier ✅ │ │ 设置 contextTargetIdentifier ✅ │ │ ❌ 没有清除 contextIsCrossBlock! │ │ ❌ 没有清除 contextAffectedSubBlocks! │ └─────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────┐ │ 用户点击"高亮"按钮 │ ├─────────────────────────────────────────────────┤ │ 读取 contextIsCrossBlock = "true" ❌ (旧值) │ │ 调用 handleCrossBlockMenuAction ❌ │ │ 使用 contextAffectedSubBlocks = [8个ID] ❌ (旧值│ │ → 高亮了错误的 8 个子块! │ └─────────────────────────────────────────────────┘ ``` --- ## ✅ 修复方案 ### 修复代码 **文件**: `annotation_logic.js:740-743` (新增) ```javascript // 🔧 BUG FIX: 清除跨子块相关属性,避免单子块操作时误用旧的跨子块数据 delete annotationContextMenuElement.dataset.contextIsCrossBlock; delete annotationContextMenuElement.dataset.contextCrossBlockAnnotationId; delete annotationContextMenuElement.dataset.contextAffectedSubBlocks; ``` ### 修复位置 在单子块右键事件处理中(`annotation_logic.js:708-766`),设置完其他属性后,**立即清除**跨子块相关属性。 ### 修复后的流程 ``` 第 1 次操作:跨子块高亮 → contextIsCrossBlock = "true" ✅ 第 2 次操作:单子块高亮 → 清除 contextIsCrossBlock ✅ → 清除 contextAffectedSubBlocks ✅ → 设置 contextTargetIdentifier = "109.0" ✅ 点击高亮按钮 → contextIsCrossBlock === undefined ✅ → isCrossBlockOperation = false ✅ → 执行单子块高亮 ✅ ``` --- ## 🧪 测试验证 ### 测试场景 1. **场景 A: 跨子块 → 单子块** - 先跨 5 个子块高亮 - 再在单个子块内选择文本高亮 - **预期**: 只高亮选中的文本 ✅ 2. **场景 B: 单子块 → 跨子块** - 先单子块高亮 - 再跨多个子块高亮 - **预期**: 正确高亮多个子块 ✅ 3. **场景 C: 连续单子块操作** - 连续在不同段落做单子块高亮 - **预期**: 每次都正确高亮 ✅ ### 测试步骤 ```bash # 1. 清除缓存并刷新 Ctrl + Shift + R # 2. 打开历史详情页 # 3. 先选中多个段落 → 右键 → 高亮 # 4. 再选中单个段落内的部分文本 → 右键 → 高亮 # 5. 观察控制台日志 ``` **预期日志** ✅: ``` [跨子块检测] 选择在同一个子块内 [AnnotationLogic] 单子块高亮操作... (不应该出现 "[跨子块操作] 执行操作") ``` --- ## 📊 影响评估 ### 严重程度: 🔴 高 **原因**: - 影响批注核心功能 - 可能导致错误的高亮范围 - 用户体验差(高亮了不该高亮的内容) ### 影响范围 | 功能 | 是否受影响 | 影响程度 | |------|-----------|---------| | 跨子块高亮 | ✅ 是 | 🔴 高 | | 单子块高亮 | ✅ 是 | 🔴 高 | | 批注添加 | ✅ 是 | 🔴 高 | | 高亮移除 | ✅ 是 | 🟠 中 | | 其他功能 | ❌ 否 | - | ### 复现条件 - ✅ 必须先做过跨子块操作 - ✅ 然后做单子块操作 - ✅ 两次操作在同一个页面会话中 **复现率**: 100%(符合条件时) --- ## 🚀 部署建议 ### 优先级: 🔴 高 建议**立即部署**,原因: 1. Bug 严重影响批注核心功能 2. 修复简单(3 行代码),风险极低 3. 不影响其他功能 ### 回归测试清单 ``` [ ] 跨子块高亮 → 单子块高亮 [ ] 单子块高亮 → 跨子块高亮 [ ] 连续多次单子块操作 [ ] 跨子块批注添加 [ ] 单子块批注添加 [ ] 高亮移除 [ ] 切换标签后批注恢复 ``` --- ## 📝 相关 Issue ### 用户报告 > "我选中某个段落里面有公式的情况,就会出错" **根源不是公式**,而是: - 用户之前可能选中了包含公式的**多个段落**(跨子块) - 然后选中单个段落内的文本(单子块) - 触发了状态污染 bug **公式只是触发场景之一**,任何跨子块 → 单子块操作都会触发。 --- ## 🔮 预防措施 ### 代码模式 **原则**: 每次设置上下文菜单的 dataset 时,**清除所有可能的旧状态** **推荐模式**: ```javascript // 清除所有状态 function clearContextMenuState() { const keys = Object.keys(annotationContextMenuElement.dataset); keys.filter(k => k.startsWith('context')).forEach(k => { delete annotationContextMenuElement.dataset[k]; }); } // 设置新状态前先清除 clearContextMenuState(); annotationContextMenuElement.dataset.contextXXX = newValue; ``` ### 未来改进 **建议**: 使用状态机管理批注上下文,而不是依赖 dataset ```javascript class AnnotationContext { constructor() { this.reset(); } reset() { this.isCrossBlock = false; this.affectedSubBlocks = []; this.targetIdentifier = null; // ... } setCrossBlock(data) { this.reset(); this.isCrossBlock = true; this.affectedSubBlocks = data.subBlocks; // ... } setSingleBlock(data) { this.reset(); // 自动清除旧状态 this.isCrossBlock = false; this.targetIdentifier = data.id; // ... } } ``` --- ## ✅ 验收清单 - [x] Bug 根源分析完成 - [x] 修复代码实施完成 - [x] 语法检查通过 - [ ] 功能测试通过 - [ ] 回归测试通过 - [ ] 用户验证通过 - [ ] 部署到生产环境 --- ## 📌 总结 **Bug**: 跨子块批注状态污染单子块操作 **根源**: 单子块处理时未清除跨子块相关的 dataset 属性 **修复**: 添加 3 行代码清除旧状态 **影响**: 批注核心功能 **优先级**: 🔴 高,建议立即部署 --- **修复完成!** 🎉 感谢用户的详细反馈,这个 bug 发现得非常及时!