338 lines
9.5 KiB
Markdown
338 lines
9.5 KiB
Markdown
# 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 发现得非常及时!
|