# 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 发现得非常及时！
