From c5d956d3a8357815c457eac3277f60673f8aacb6 Mon Sep 17 00:00:00 2001
From: AfyerCu <20569838@qq.com>
Date: Wed, 5 Nov 2025 20:02:46 +0800
Subject: [PATCH] =?UTF-8?q?refactor:=20=E9=87=8D=E5=88=B6=E8=80=81?=
=?UTF-8?q?=E5=B8=88=E7=AB=AF=E9=98=85=E5=8D=B7?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.task/dom-rendering-test-guide.md | 576 +++++++++++++++++
.task/konva-to-dom-migration.md | 270 ++++++++
.../components/renderer/DomImageRenderer.vue | 457 ++++++++++++++
.../renderer/MarkingImageViewerNew.vue | 11 +-
.../components/renderer/QuestionRenderer.vue | 29 +-
.../components/renderer/TraceToolbar.vue | 2 +-
.../composables/renderer/useMarkingCommand.ts | 70 ++-
.../composables/renderer/useMarkingDom.ts | 579 ++++++++++++++++++
.../composables/renderer/useMarkingTools.ts | 11 +-
.../marking/review/ReviewImageRenderer.vue | 6 +-
static/app/icons/1024x1024.png | Bin 0 -> 59336 bytes
static/app/icons/120x120.png | Bin 0 -> 3231 bytes
static/app/icons/144x144.png | Bin 0 -> 3791 bytes
static/app/icons/152x152.png | Bin 0 -> 3981 bytes
static/app/icons/167x167.png | Bin 0 -> 4479 bytes
static/app/icons/180x180.png | Bin 0 -> 4844 bytes
static/app/icons/192x192.png | Bin 0 -> 5319 bytes
static/app/icons/20x20.png | Bin 0 -> 574 bytes
static/app/icons/29x29.png | Bin 0 -> 780 bytes
static/app/icons/40x40.png | Bin 0 -> 985 bytes
static/app/icons/58x58.png | Bin 0 -> 1483 bytes
static/app/icons/60x60.png | Bin 0 -> 1521 bytes
static/app/icons/72x72.png | Bin 0 -> 1802 bytes
static/app/icons/76x76.png | Bin 0 -> 1972 bytes
static/app/icons/80x80.png | Bin 0 -> 2060 bytes
static/app/icons/87x87.png | Bin 0 -> 2289 bytes
static/app/icons/96x96.png | Bin 0 -> 2396 bytes
vite.config.ts | 3 -
28 files changed, 1974 insertions(+), 40 deletions(-)
create mode 100644 .task/dom-rendering-test-guide.md
create mode 100644 .task/konva-to-dom-migration.md
create mode 100644 src/components/marking/components/renderer/DomImageRenderer.vue
create mode 100644 src/components/marking/composables/renderer/useMarkingDom.ts
create mode 100644 static/app/icons/1024x1024.png
create mode 100644 static/app/icons/120x120.png
create mode 100644 static/app/icons/144x144.png
create mode 100644 static/app/icons/152x152.png
create mode 100644 static/app/icons/167x167.png
create mode 100644 static/app/icons/180x180.png
create mode 100644 static/app/icons/192x192.png
create mode 100644 static/app/icons/20x20.png
create mode 100644 static/app/icons/29x29.png
create mode 100644 static/app/icons/40x40.png
create mode 100644 static/app/icons/58x58.png
create mode 100644 static/app/icons/60x60.png
create mode 100644 static/app/icons/72x72.png
create mode 100644 static/app/icons/76x76.png
create mode 100644 static/app/icons/80x80.png
create mode 100644 static/app/icons/87x87.png
create mode 100644 static/app/icons/96x96.png
diff --git a/.task/dom-rendering-test-guide.md b/.task/dom-rendering-test-guide.md
new file mode 100644
index 0000000..3f0d896
--- /dev/null
+++ b/.task/dom-rendering-test-guide.md
@@ -0,0 +1,576 @@
+# DOM 渲染功能测试指南
+
+## 🎯 测试目标
+
+验证从 Konva Canvas 迁移到 DOM 渲染后,所有功能正常工作,特别是在微信小程序环境中。
+
+## 🔧 测试环境
+
+### 浏览器环境
+- Chrome/Edge (开发调试)
+- Safari (iOS 兼容性)
+- 微信开发者工具 (小程序环境)
+
+### 设备
+- 桌面端 (鼠标操作)
+- 移动端 (触摸操作)
+- 平板 (双指缩放)
+
+## ✅ 功能测试清单
+
+### 1. 基础绘制功能
+
+#### 1.1 矩形工具
+**测试步骤**:
+1. 选择矩形工具
+2. 在画布上按下鼠标/手指
+3. 拖动到目标位置
+4. 释放鼠标/手指
+
+**预期结果**:
+- ✅ 矩形正确绘制
+- ✅ 矩形位置、大小准确
+- ✅ 矩形颜色、边框宽度符合设置
+- ✅ 可以绘制负方向矩形(从右下往左上拖)
+
+**测试数据**:
+```typescript
+// 预期数据结构
+{
+ id: "shape_xxx",
+ type: "rect",
+ x: 100,
+ y: 100,
+ width: 200,
+ height: 150,
+ stroke: "#ff0000",
+ strokeWidth: 2
+}
+```
+
+#### 1.2 笔工具(线条)
+**测试步骤**:
+1. 选择笔工具
+2. 在画布上绘制任意路径
+3. 释放鼠标/手指
+
+**预期结果**:
+- ✅ 线条流畅,无断点
+- ✅ 线条颜色、粗细符合设置
+- ✅ 线条端点圆滑(lineCap: round)
+- ✅ 快速绘制时不丢失点
+
+**测试数据**:
+```typescript
+{
+ id: "shape_xxx",
+ type: "line",
+ x: 0,
+ y: 0,
+ points: [100, 100, 105, 102, 110, 105, ...],
+ stroke: "#ff0000",
+ strokeWidth: 2
+}
+```
+
+#### 1.3 文本注释
+**测试步骤**:
+1. 选择文本工具
+2. 点击画布位置
+3. 输入文本内容
+4. 确认
+
+**预期结果**:
+- ✅ 弹出输入框
+- ✅ 文本正确显示在点击位置
+- ✅ 文本大小、颜色符合设置
+- ✅ 支持多行文本
+- ✅ 支持特殊字符
+
+**测试用例**:
+- 普通文本: "扣1分"
+- 数字: "123"
+- 特殊字符: "+5", "-2"
+- 多行文本: "第一行\n第二行"
+
+#### 1.4 特殊标记
+
+**正确标记 ✓**:
+- ✅ 显示红色勾号
+- ✅ 位置准确
+- ✅ 大小适中(20x20)
+
+**错误标记 ✗**:
+- ✅ 显示红色叉号
+- ✅ 位置准确
+- ✅ 大小适中(20x20)
+
+**半对标记**:
+- ✅ 显示勾号+斜线组合
+- ✅ 位置准确
+- ✅ 大小适中(20x20)
+
+### 2. 擦除功能
+
+#### 2.1 点击擦除
+**测试步骤**:
+1. 绘制多个不同类型的标记
+2. 选择擦除工具
+3. 点击各个标记
+
+**预期结果**:
+- ✅ 点击矩形内部,矩形被删除
+- ✅ 点击线条附近(5px内),线条被删除
+- ✅ 点击特殊标记,标记被删除
+- ✅ 点击文本,文本被删除
+- ✅ 点击空白处,无反应
+
+**碰撞检测测试**:
+```typescript
+// 矩形边界测试
+点击 (100, 100) -> 在矩形内 ✅
+点击 (99, 99) -> 在矩形外 ❌
+点击 (300, 250) -> 在矩形内 ✅
+点击 (301, 251) -> 在矩形外 ❌
+
+// 线条距离测试
+点击距离线条 3px -> 命中 ✅
+点击距离线条 10px -> 未命中 ❌
+
+// 特殊标记测试(中心点 100, 100)
+点击 (100, 100) -> 命中 ✅
+点击 (90, 90) -> 命中 ✅
+点击 (110, 110) -> 命中 ✅
+点击 (89, 89) -> 未命中 ❌
+```
+
+#### 2.2 拖动擦除
+**测试步骤**:
+1. 绘制多个标记
+2. 选择擦除工具
+3. 按住鼠标/手指拖动经过标记
+
+**预期结果**:
+- ✅ 经过的标记被连续删除
+- ✅ 擦除流畅,无卡顿
+
+### 3. 撤销/重做功能
+
+#### 3.1 撤销操作
+**测试步骤**:
+1. 依次绘制:矩形 -> 线条 -> 文本
+2. 点击撤销按钮 3 次
+
+**预期结果**:
+- ✅ 第1次撤销:文本消失
+- ✅ 第2次撤销:线条消失
+- ✅ 第3次撤销:矩形消失
+- ✅ 第4次撤销:按钮禁用,无反应
+
+**命令栈验证**:
+```typescript
+// 初始状态
+commandStack: []
+currentIndex: -1
+canUndo: false
+
+// 绘制矩形后
+commandStack: [AddShapeCommand(rect)]
+currentIndex: 0
+canUndo: true
+
+// 绘制线条后
+commandStack: [AddShapeCommand(rect), AddShapeCommand(line)]
+currentIndex: 1
+canUndo: true
+
+// 撤销一次后
+commandStack: [AddShapeCommand(rect), AddShapeCommand(line)]
+currentIndex: 0
+canUndo: true
+canRedo: true
+```
+
+#### 3.2 重做操作
+**测试步骤**:
+1. 绘制标记
+2. 撤销
+3. 点击重做
+
+**预期结果**:
+- ✅ 标记重新出现
+- ✅ 位置、样式与原来一致
+
+#### 3.3 撤销后新建
+**测试步骤**:
+1. 绘制:A -> B -> C
+2. 撤销 2 次(剩下 A)
+3. 绘制新标记 D
+
+**预期结果**:
+- ✅ B、C 从命令栈中移除
+- ✅ 不能重做 B、C
+- ✅ 可以撤销 D
+
+```typescript
+// 撤销2次后
+commandStack: [A, B, C]
+currentIndex: 0
+
+// 绘制D后
+commandStack: [A, D] // B、C被截断
+currentIndex: 1
+canRedo: false
+```
+
+#### 3.4 清空所有标记
+**测试步骤**:
+1. 绘制多个标记
+2. 点击"清空"按钮
+3. 点击撤销
+
+**预期结果**:
+- ✅ 所有标记消失
+- ✅ 撤销后所有标记恢复
+
+### 4. 缩放功能
+
+#### 4.1 手动缩放
+**测试步骤**:
+1. 设置缩放比例为 0.5
+2. 绘制标记
+3. 设置缩放比例为 2.0
+
+**预期结果**:
+- ✅ 图片大小正确变化
+- ✅ 标记跟随图片缩放
+- ✅ 标记位置相对图片不变
+- ✅ 新绘制的标记坐标正确
+
+**坐标转换验证**:
+```typescript
+// 缩放 0.5 时点击屏幕 (100, 100)
+实际坐标 = (100 / 0.5, 100 / 0.5) = (200, 200)
+
+// 缩放 2.0 时点击屏幕 (100, 100)
+实际坐标 = (100 / 2.0, 100 / 2.0) = (50, 50)
+```
+
+#### 4.2 自适应宽度模式
+**测试步骤**:
+1. 启用自适应宽度
+2. 调整浏览器窗口宽度
+
+**预期结果**:
+- ✅ 图片宽度自动适应容器
+- ✅ 高度按比例缩放
+- ✅ 标记位置正确
+
+#### 4.3 双指缩放(触摸设备)
+**测试步骤**:
+1. 在触摸设备上打开
+2. 双指捏合/展开
+
+**预期结果**:
+- ✅ 图片跟随手势缩放
+- ✅ 显示缩放百分比提示
+- ✅ 标记跟随缩放
+
+### 5. 快捷打分功能
+
+#### 5.1 加分模式
+**测试步骤**:
+1. 设置快捷打分:加分模式,分值 2
+2. 启用快捷打分点击模式
+3. 点击画布
+
+**预期结果**:
+- ✅ 显示 "+2" 文本标记
+- ✅ 分数增加 2 分
+- ✅ 文本颜色为红色
+- ✅ 不能超过满分
+
+**边界测试**:
+```typescript
+满分: 10
+当前: 9
+点击加2 -> 分数变为 10 ✅
+再次点击 -> 无反应 ✅
+```
+
+#### 5.2 减分模式
+**测试步骤**:
+1. 设置快捷打分:减分模式,分值 1
+2. 启用快捷打分点击模式
+3. 点击画布
+
+**预期结果**:
+- ✅ 显示 "-1" 文本标记
+- ✅ 分数减少 1 分
+- ✅ 文本颜色为蓝色
+- ✅ 不能低于 0 分
+
+### 6. 多图片场景
+
+#### 6.1 多图片渲染
+**测试步骤**:
+1. 加载包含 3 张图片的题目
+2. 在每张图片上绘制不同标记
+
+**预期结果**:
+- ✅ 每张图片独立渲染
+- ✅ 标记数据互不干扰
+- ✅ 撤销操作针对最后操作的图片
+
+#### 6.2 横向/纵向布局
+**测试步骤**:
+1. 切换图片布局模式
+
+**预期结果**:
+- ✅ 横向布局:图片水平排列
+- ✅ 纵向布局:图片垂直排列
+- ✅ 标记位置不受影响
+
+### 7. 只读模式
+
+#### 7.1 历史记录查看
+**测试步骤**:
+1. 打开已批改的试卷
+2. 查看标记
+
+**预期结果**:
+- ✅ 所有标记正确显示
+- ✅ 不响应点击事件
+- ✅ 不显示工具栏
+
+### 8. 数据持久化
+
+#### 8.1 数据导出
+**测试步骤**:
+1. 绘制多种标记
+2. 导出数据
+
+**预期结果**:
+```json
+{
+ "version": "1.0",
+ "shapes": [
+ {
+ "id": "shape_xxx",
+ "type": "rect",
+ "x": 100,
+ "y": 100,
+ "width": 200,
+ "height": 150,
+ "stroke": "#ff0000",
+ "strokeWidth": 2
+ }
+ ],
+ "specialMarks": [
+ {
+ "id": "mark_xxx",
+ "type": "correct",
+ "x": 300,
+ "y": 200
+ }
+ ],
+ "annotations": [
+ {
+ "id": "text_xxx",
+ "x": 400,
+ "y": 300,
+ "text": "扣1分",
+ "fontSize": 14,
+ "color": "#000000"
+ }
+ ]
+}
+```
+
+#### 8.2 数据导入
+**测试步骤**:
+1. 导入上述 JSON 数据
+2. 查看渲染结果
+
+**预期结果**:
+- ✅ 所有标记正确恢复
+- ✅ 位置、样式一致
+
+### 9. 性能测试
+
+#### 9.1 大量标记渲染
+**测试步骤**:
+1. 导入包含 100+ 标记的数据
+2. 观察渲染性能
+
+**预期结果**:
+- ✅ 初始渲染 < 1s
+- ✅ 滚动流畅,无卡顿
+- ✅ 内存占用合理
+
+#### 9.2 快速连续绘制
+**测试步骤**:
+1. 快速绘制多条线条
+
+**预期结果**:
+- ✅ 线条流畅,无延迟
+- ✅ 不丢失点
+- ✅ 不出现重复标记
+
+### 10. 兼容性测试
+
+#### 10.1 微信小程序
+**测试步骤**:
+1. 在微信开发者工具中运行
+2. 测试所有功能
+
+**预期结果**:
+- ✅ 所有功能正常
+- ✅ 触摸事件响应正常
+- ✅ 无控制台错误
+
+#### 10.2 不同分辨率
+**测试设备**:
+- iPhone SE (375x667)
+- iPhone 14 Pro (393x852)
+- iPad (768x1024)
+- Desktop (1920x1080)
+
+**预期结果**:
+- ✅ 布局自适应
+- ✅ 标记大小合适
+- ✅ 触摸区域足够大
+
+## 🐛 Bug 报告模板
+
+```markdown
+### Bug 描述
+简要描述问题
+
+### 复现步骤
+1. 步骤1
+2. 步骤2
+3. 步骤3
+
+### 预期行为
+应该发生什么
+
+### 实际行为
+实际发生了什么
+
+### 环境信息
+- 设备: iPhone 14
+- 系统: iOS 16.0
+- 浏览器: 微信开发者工具
+- 版本: 1.0.0
+
+### 截图/录屏
+如果可能,提供截图或录屏
+
+### 相关数据
+如果涉及数据问题,提供 JSON 数据
+```
+
+## 📊 测试报告模板
+
+```markdown
+# DOM 渲染功能测试报告
+
+## 测试概况
+- 测试日期: 2024-01-01
+- 测试人员: XXX
+- 测试环境: 微信开发者工具 / Chrome
+- 测试版本: 1.0.0
+
+## 测试结果汇总
+| 功能模块 | 测试用例数 | 通过 | 失败 | 通过率 |
+|---------|-----------|------|------|--------|
+| 基础绘制 | 10 | 10 | 0 | 100% |
+| 擦除功能 | 5 | 5 | 0 | 100% |
+| 撤销重做 | 8 | 8 | 0 | 100% |
+| 缩放功能 | 6 | 6 | 0 | 100% |
+| 快捷打分 | 4 | 4 | 0 | 100% |
+| 多图片 | 3 | 3 | 0 | 100% |
+| 只读模式 | 2 | 2 | 0 | 100% |
+| 数据持久化 | 4 | 4 | 0 | 100% |
+| 性能测试 | 3 | 3 | 0 | 100% |
+| 兼容性 | 5 | 5 | 0 | 100% |
+| **总计** | **50** | **50** | **0** | **100%** |
+
+## 详细测试结果
+[详细记录每个测试用例的结果]
+
+## 发现的问题
+[列出发现的所有问题]
+
+## 建议
+[提出改进建议]
+
+## 结论
+✅ 通过 / ❌ 不通过
+```
+
+## 🎯 自动化测试(可选)
+
+### 单元测试示例
+```typescript
+import { describe, it, expect } from 'vitest'
+import { useSimpleDomLayer } from './useMarkingDom'
+
+describe('useMarkingDom', () => {
+ it('should add shape correctly', () => {
+ const layer = useSimpleDomLayer({...})
+
+ // 模拟绘制矩形
+ layer.handleMouseDown(mockEvent, mockRect)
+ layer.handleMouseMove(mockEvent, mockRect)
+ layer.handleMouseUp()
+
+ expect(layer.markingData.value.shapes).toHaveLength(1)
+ expect(layer.markingData.value.shapes[0].type).toBe('rect')
+ })
+
+ it('should undo correctly', () => {
+ const layer = useSimpleDomLayer({...})
+
+ // 添加标记
+ layer.handleMouseDown(mockEvent, mockRect)
+ layer.handleMouseUp()
+
+ expect(layer.markingData.value.shapes).toHaveLength(1)
+
+ // 撤销
+ layer.undo()
+
+ expect(layer.markingData.value.shapes).toHaveLength(0)
+ })
+})
+```
+
+## 📝 测试记录
+
+### 测试日期: ___________
+### 测试人员: ___________
+
+| 测试项 | 状态 | 备注 |
+|--------|------|------|
+| 绘制矩形 | ⬜ | |
+| 绘制线条 | ⬜ | |
+| 文本注释 | ⬜ | |
+| 特殊标记 | ⬜ | |
+| 擦除功能 | ⬜ | |
+| 撤销操作 | ⬜ | |
+| 重做操作 | ⬜ | |
+| 清空标记 | ⬜ | |
+| 手动缩放 | ⬜ | |
+| 自适应宽度 | ⬜ | |
+| 快捷打分 | ⬜ | |
+| 多图片 | ⬜ | |
+| 只读模式 | ⬜ | |
+| 数据导出 | ⬜ | |
+| 数据导入 | ⬜ | |
+| 性能测试 | ⬜ | |
+| 小程序兼容 | ⬜ | |
+
+**签名**: ___________
+
diff --git a/.task/konva-to-dom-migration.md b/.task/konva-to-dom-migration.md
new file mode 100644
index 0000000..eba05f6
--- /dev/null
+++ b/.task/konva-to-dom-migration.md
@@ -0,0 +1,270 @@
+# Konva 到 DOM 渲染迁移文档
+
+## 📋 迁移概述
+
+### 迁移原因
+微信小程序不支持 Konva.js,需要将基于 Canvas 的渲染方案改为纯 DOM + SVG 渲染方案。
+
+### 迁移目标
+- ✅ 保留所有现有功能
+- ✅ 使用 Vue 响应式系统管理渲染
+- ✅ 保持命令模式的撤销/重做功能
+- ✅ 兼容微信小程序环境
+
+## 🔄 核心变更
+
+### 1. 新增文件
+
+#### `useMarkingDom.ts`
+替代 `useMarkingKonva.ts`,使用 DOM 渲染:
+- **数据结构**: `DomMarkingData` (兼容原 `KonvaMarkingData`)
+- **工具枚举**: `MarkingTool` (保持不变)
+- **核心函数**: `useSimpleDomLayer` 替代 `useSimpleKonvaLayer`
+
+**主要特性**:
+- 使用 Vue 响应式系统自动渲染
+- 鼠标/触摸事件处理
+- 坐标转换(支持缩放和偏移)
+- 碰撞检测(用于擦除功能)
+
+#### `DomImageRenderer.vue`
+替代 `KonvaImageRenderer.vue`,使用 DOM 渲染:
+- **背景图片**: `` 标签
+- **矩形**: `