@zzalai/leafer-point-annotation 1.0.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -2
- package/README_EN.md +57 -2
- package/docs/assets/{index-L8gL3x2V.js → index-CPn8AE3g.js} +1 -1
- package/docs/assets/index-Dqqq7qvI.css +1 -0
- package/docs/index.html +2 -2
- package/package.json +9 -3
- package/project-docs/ARCHITECTURE.md +47 -4
- package/project-docs/IMPLEMENTATION_PLAN.md +37 -19
- package/project-docs/REQUIREMENTS.md +27 -8
- package/src/App.vue +8 -5
- package/src/components/PointAnnotation.vue +224 -25
- package/tsconfig.json +5 -1
- package/vite.config.ts +6 -1
- package/docs/assets/index-DGiYiG5f.css +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.brush-panel-overlay[data-v-10bc9982]{z-index:1500;background:#0000004d;position:fixed;inset:0}.brush-style-panel[data-v-10bc9982]{z-index:1501;background:#fff;border-radius:10px;min-width:240px;position:fixed;overflow:visible;box-shadow:0 4px 24px #00000026}.panel-header[data-v-10bc9982]{background:var(--leafer-point-color-background-light);border-bottom:1px solid var(--leafer-point-color-border);border-radius:10px 10px 0 0;justify-content:space-between;align-items:center;padding:10px 16px;display:flex}.panel-header span[data-v-10bc9982]{color:var(--leafer-point-color-text);font-size:14px;font-weight:600}.close-btn[data-v-10bc9982]{cursor:pointer;width:24px;height:24px;color:var(--leafer-point-color-text);background:0 0;border:none;border-radius:50%;justify-content:center;align-items:center;font-size:18px;transition:all .2s;display:flex}.close-btn[data-v-10bc9982]:hover{background:var(--leafer-point-color-border)}.panel-content[data-v-10bc9982]{padding:16px 16px 24px}.config-item[data-v-10bc9982]{align-items:center;margin-bottom:20px;display:flex}.config-item[data-v-10bc9982]:last-child{margin-bottom:0}.config-label[data-v-10bc9982]{color:var(--leafer-point-color-text);text-align:right;min-width:50px;padding-right:5px;font-size:12px;display:block}.config-value[data-v-10bc9982]{color:var(--leafer-point-color-text);width:30px;padding-left:5px;font-size:12px}.color-picker-wrapper[data-v-10bc9982]{width:100%;margin:-10px 0}.config-slider[data-v-10bc9982]{appearance:none;cursor:pointer;background:#e0e0e0;border-radius:3px;outline:none;width:200px;height:6px}.config-slider[data-v-10bc9982]::-webkit-slider-thumb{appearance:none;background:var(--leafer-point-color-primary);cursor:pointer;border:2px solid #fff;border-radius:50%;width:16px;height:16px;box-shadow:0 2px 4px #0003}.config-slider[data-v-10bc9982]::-moz-range-thumb{background:var(--leafer-point-color-primary);cursor:pointer;border:2px solid #fff;border-radius:50%;width:16px;height:16px;box-shadow:0 2px 4px #0003}:root{--leafer-point-color-primary:#007aff;--leafer-point-color-background:#f5f5f5;--leafer-point-color-background-light:#f0f0f0;--leafer-point-color-white:#fff;--leafer-point-color-text:#333;--leafer-point-color-text-secondary:#666;--leafer-point-color-text-tertiary:#999;--leafer-point-color-placeholder:#999;--leafer-point-color-border:#ddd;--leafer-point-color-border-light:#e0e0e0;--leafer-point-color-error:#e74c3c;--leafer-point-color-button:#3498db;--leafer-point-color-button-rgb:52, 152, 219;--leafer-point-color-button-hover:#2980b9;--leafer-point-padding-toolbar:10px;--leafer-point-padding-tool-button:8px;--leafer-point-size-tool-icon:18px;--leafer-point-size-zoom-button:36px;--leafer-point-size-zoom-value:60px;--leafer-point-font-size-hotkey:10px;--leafer-point-padding-hotkey:1px 3px;--leafer-point-padding-error:20px;--leafer-point-padding-error-button:8px 16px;--leafer-point-border-radius-tool-button:4px;--leafer-point-border-radius-hotkey:2px;--leafer-point-border-radius-overlay:8px;--leafer-point-border-radius-zoom:8px;--leafer-point-shadow-tool-button:0 2px 4px #0000001a;--leafer-point-shadow-tool-button-active:0 2px 4px #007aff4d;--leafer-point-shadow-tool-button-hover:0 4px 6px #0000001a;--leafer-point-shadow-overlay:0 4px 12px #0000001a;--leafer-point-shadow-zoom:0 2px 8px #00000026;--leafer-point-transition-time:.2s;--leafer-point-animation-gradient:2s}.point-annotation[data-v-f02f10ad]{width:100%;height:100%}.canvas-container[data-v-f02f10ad]{outline:none;width:100%;height:100%;position:relative;overflow:hidden}.point-annotation.has-image .canvas-container[data-v-f02f10ad]{height:calc(100% - 55px)}.canvas-container[data-v-f02f10ad]:focus{outline:2px solid var(--leafer-point-color-primary);outline-offset:-2px}.loading-overlay[data-v-f02f10ad]{background-color:var(--leafer-point-color-background-light);border-radius:var(--leafer-point-border-radius-overlay);box-shadow:var(--leafer-point-shadow-overlay);z-index:1000;justify-content:center;align-items:center;min-width:100%;min-height:100%;display:flex;position:absolute;top:50%;left:50%;overflow:hidden;transform:translate(-50%,-50%)}.gradient-animation[data-v-f02f10ad]{animation:gradientShift-f02f10ad var(--leafer-point-animation-gradient) ease-in-out infinite;opacity:.7;background:linear-gradient(135deg,#667eea 0%,#764ba2 100%) 0 0/200% 200%;position:absolute;inset:0}@keyframes gradientShift-f02f10ad{0%{background-position:0%}50%{background-position:100%}to{background-position:0%}}.loading-text[data-v-f02f10ad]{z-index:1;color:#fff;text-shadow:0 2px 4px #0003;font-size:18px;font-weight:500;position:relative}.upload-overlay[data-v-f02f10ad]{background-color:var(--leafer-point-color-white);z-index:1000;border:3px dashed var(--leafer-point-color-border);flex-direction:column;justify-content:center;align-items:center;transition:all .2s;display:flex;position:absolute;inset:0}.upload-icon[data-v-f02f10ad]{color:var(--leafer-point-color-placeholder);margin-bottom:24px;transform:scale(1.2)}.upload-text[data-v-f02f10ad]{color:var(--leafer-point-color-text);margin-bottom:12px;font-size:18px;font-weight:500}.upload-hint[data-v-f02f10ad]{color:var(--leafer-point-color-placeholder);margin-bottom:28px;font-size:14px}.upload-button[data-v-f02f10ad]{background-color:var(--leafer-point-color-button);color:#fff;border-radius:var(--leafer-point-border-radius-tool-button);cursor:pointer;border:none;padding:12px 32px;font-size:15px;font-weight:500;transition:all .2s}.upload-button[data-v-f02f10ad]:hover{background-color:var(--leafer-point-color-button-hover);transform:translateY(-1px)}.upload-overlay.drag-over[data-v-f02f10ad]{border-color:var(--leafer-point-color-button);background-color:rgba(var(--leafer-point-color-button-rgb), .05)}.upload-overlay.drag-over .upload-icon[data-v-f02f10ad]{color:var(--leafer-point-color-button)}.error-overlay[data-v-f02f10ad]{background-color:var(--leafer-point-color-white);z-index:1000;flex-direction:column;justify-content:center;align-items:center;display:flex;position:absolute;inset:0}.error-overlay p[data-v-f02f10ad]{color:var(--leafer-point-color-error);margin-bottom:24px;font-size:18px;font-weight:500}.error-overlay button[data-v-f02f10ad]{background-color:var(--leafer-point-color-button);color:#fff;border-radius:var(--leafer-point-border-radius-tool-button);cursor:pointer;border:none;padding:12px 32px;font-size:15px;font-weight:500;transition:all .2s}.error-overlay button[data-v-f02f10ad]:hover{background-color:var(--leafer-point-color-button-hover);transform:translateY(-1px)}.zoom-controller[data-v-f02f10ad]{background-color:var(--leafer-point-color-white);border-radius:var(--leafer-point-border-radius-zoom);box-shadow:var(--leafer-point-shadow-zoom);z-index:100;align-items:center;display:flex;position:absolute;bottom:16px;left:16px;overflow:hidden}.zoom-button[data-v-f02f10ad]{width:var(--leafer-point-size-zoom-button);height:var(--leafer-point-size-zoom-button);background-color:var(--leafer-point-color-white);color:var(--leafer-point-color-text);cursor:pointer;transition:all var(--leafer-point-transition-time) ease;border:none;justify-content:center;align-items:center;display:flex;position:relative}.zoom-button[data-v-f02f10ad]:hover{background-color:var(--leafer-point-color-background-light);color:var(--leafer-point-color-primary)}.zoom-button[data-v-f02f10ad]:active{background-color:#e0e0e0}.zoom-value[data-v-f02f10ad]{min-width:var(--leafer-point-size-zoom-value);height:var(--leafer-point-size-zoom-button);line-height:var(--leafer-point-size-zoom-button);text-align:center;color:var(--leafer-point-color-text);cursor:pointer;border-left:1px solid var(--leafer-point-color-border-light);border-right:1px solid var(--leafer-point-color-border-light);transition:all var(--leafer-point-transition-time) ease;font-size:14px;font-weight:500;position:relative}.zoom-value .hotkey-hint[data-v-f02f10ad]{line-height:1}.zoom-value[data-v-f02f10ad]:hover{background-color:var(--leafer-point-color-background-light);color:var(--leafer-point-color-primary)}.toolbar[data-v-f02f10ad]{padding:var(--leafer-point-padding-toolbar);background-color:var(--leafer-point-color-background);border-top:1px solid var(--leafer-point-color-border);justify-content:center;align-items:center;gap:10px;display:flex}.tool-button[data-v-f02f10ad]{padding:var(--leafer-point-padding-tool-button);border-radius:var(--leafer-point-border-radius-tool-button);background-color:var(--leafer-point-color-white);color:var(--leafer-point-color-text);cursor:pointer;transition:all var(--leafer-point-transition-time) ease;box-shadow:var(--leafer-point-shadow-tool-button);border:none;justify-content:center;align-items:center;display:flex;position:relative}.tool-button[data-v-f02f10ad]:hover{background-color:var(--leafer-point-color-background-light);color:var(--leafer-point-color-primary);box-shadow:var(--leafer-point-shadow-tool-button-hover)}.tool-button[data-v-f02f10ad]:active{box-shadow:var(--leafer-point-shadow-tool-button);transform:translateY(1px)}.tool-button.active[data-v-f02f10ad]{background-color:var(--leafer-point-color-primary);color:#fff;box-shadow:var(--leafer-point-shadow-tool-button-active)}.hotkey-hint[data-v-f02f10ad]{font-size:var(--leafer-point-font-size-hotkey);color:#fff;padding:var(--leafer-point-padding-hotkey);border-radius:var(--leafer-point-border-radius-hotkey);pointer-events:none;white-space:nowrap;background-color:#0009;position:absolute;top:0;right:0}.size-control[data-v-f02f10ad]{padding:var(--leafer-point-padding-tool-button);background-color:var(--leafer-point-color-white);border-radius:var(--leafer-point-border-radius-tool-button);box-shadow:var(--leafer-point-shadow-tool-button);align-items:center;gap:8px;display:flex}.size-label[data-v-f02f10ad]{color:var(--leafer-point-color-text);white-space:nowrap;font-size:12px}.size-slider[data-v-f02f10ad]{appearance:none;cursor:pointer;background:#e0e0e0;border-radius:4px;outline:none;width:120px;height:8px}.size-slider[data-v-f02f10ad]::-webkit-slider-thumb{appearance:none;background:var(--leafer-point-color-primary);cursor:pointer;width:18px;height:18px;transition:all var(--leafer-point-transition-time) ease;border:2px solid #fff;border-radius:50%;box-shadow:0 2px 6px #0000004d}.size-slider[data-v-f02f10ad]::-webkit-slider-thumb:hover{background:var(--leafer-point-color-primary-hover);transform:scale(1.15);box-shadow:0 3px 8px #0006}.size-slider[data-v-f02f10ad]::-webkit-slider-thumb:active{background:var(--leafer-point-color-primary-hover);transform:scale(1.15);box-shadow:0 3px 8px #0006}.size-slider[data-v-f02f10ad]:focus::-webkit-slider-thumb{background:var(--leafer-point-color-primary-hover);transform:scale(1.15);box-shadow:0 3px 8px #0006}.size-slider[data-v-f02f10ad]::-moz-range-thumb{background:var(--leafer-point-color-primary);cursor:pointer;width:18px;height:18px;transition:all var(--leafer-point-transition-time) ease;border:2px solid #fff;border-radius:50%;box-shadow:0 2px 6px #0000004d}.size-slider[data-v-f02f10ad]::-moz-range-thumb:hover{background:var(--leafer-point-color-primary-hover);transform:scale(1.15);box-shadow:0 3px 8px #0006}.size-slider[data-v-f02f10ad]::-moz-range-thumb:active{background:var(--leafer-point-color-primary-hover);transform:scale(1.15);box-shadow:0 3px 8px #0006}.size-slider[data-v-f02f10ad]:focus::-moz-range-thumb{background:var(--leafer-point-color-primary-hover);transform:scale(1.15);box-shadow:0 3px 8px #0006}.size-slider[data-v-f02f10ad]:focus{outline:none}.size-value[data-v-f02f10ad]{text-align:center;min-width:30px;color:var(--leafer-point-color-primary);font-size:12px;font-weight:600}.app[data-v-4ddc095b]{max-width:1200px;margin:0 auto;padding:20px;font-family:Arial,sans-serif}h1[data-v-4ddc095b]{text-align:center;margin-bottom:30px}.editor-container[data-v-4ddc095b]{border:1px solid #ddd;border-radius:8px;width:100%;height:600px;margin-bottom:30px;overflow:hidden}.controls[data-v-4ddc095b]{background-color:#f5f5f5;border-radius:8px;margin-bottom:30px;padding:20px}.control-group[data-v-4ddc095b]{margin-bottom:15px}label[data-v-4ddc095b]{margin-bottom:5px;font-weight:700;display:block}input[data-v-4ddc095b]{border:1px solid #ddd;border-radius:4px;width:100%;margin-bottom:10px;padding:8px}.mask-options[data-v-4ddc095b]{gap:15px;margin-bottom:10px;display:flex}.mask-options label[data-v-4ddc095b]{align-items:center;gap:5px;font-weight:400;display:flex}.mask-options select[data-v-4ddc095b]{border:1px solid #ddd;border-radius:4px;padding:4px 8px}button[data-v-4ddc095b]{color:#fff;cursor:pointer;background-color:#007bff;border:none;border-radius:4px;margin-right:10px;padding:8px 16px}button[data-v-4ddc095b]:hover{background-color:#0069d9}.output[data-v-4ddc095b]{background-color:#f5f5f5;border-radius:8px;margin-bottom:30px;padding:20px}pre[data-v-4ddc095b]{white-space:pre-wrap;word-wrap:break-word;background-color:#fff;border-radius:4px;max-height:300px;padding:15px;overflow-y:auto}.status[data-v-4ddc095b]{background-color:#f5f5f5;border-radius:8px;padding:20px}
|
package/docs/index.html
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
7
|
<title>Leafer Point Annotation</title>
|
|
8
|
-
<script type="module" crossorigin src="./assets/index-
|
|
9
|
-
<link rel="stylesheet" crossorigin href="./assets/index-
|
|
8
|
+
<script type="module" crossorigin src="./assets/index-CPn8AE3g.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="./assets/index-Dqqq7qvI.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
12
12
|
<div id="app"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zzalai/leafer-point-annotation",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "A Vue3 component for point annotation and brush painting on images using LeaferJS, supporting COCO/YOLO/JSON export, designed for AI model training dataset annotation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -14,6 +14,10 @@
|
|
|
14
14
|
"import": "./dist/leafer-point-annotation.es.js",
|
|
15
15
|
"require": "./dist/leafer-point-annotation.umd.js",
|
|
16
16
|
"types": "./dist/index.d.ts"
|
|
17
|
+
},
|
|
18
|
+
"./dist/leafer-point-annotation.css": {
|
|
19
|
+
"import": "./dist/leafer-point-annotation.css",
|
|
20
|
+
"require": "./dist/leafer-point-annotation.css"
|
|
17
21
|
}
|
|
18
22
|
},
|
|
19
23
|
"scripts": {
|
|
@@ -21,7 +25,8 @@
|
|
|
21
25
|
"build": "vite build",
|
|
22
26
|
"preview": "vite preview",
|
|
23
27
|
"type-check": "tsc --noEmit",
|
|
24
|
-
"docs:build": "vite build --config vite.docs.config.ts"
|
|
28
|
+
"docs:build": "vite build --config vite.docs.config.ts",
|
|
29
|
+
"build:all": "npm run build && npm run docs:build"
|
|
25
30
|
},
|
|
26
31
|
"keywords": [
|
|
27
32
|
"vue3",
|
|
@@ -56,9 +61,10 @@
|
|
|
56
61
|
"devDependencies": {
|
|
57
62
|
"@types/node": "^25.6.0",
|
|
58
63
|
"@vitejs/plugin-vue": "^6.0.6",
|
|
64
|
+
"@vue/language-core": "^3.3.3",
|
|
59
65
|
"terser": "^5.46.1",
|
|
60
66
|
"typescript": "^6.0.2",
|
|
61
67
|
"vite": "^8.0.8",
|
|
62
|
-
"vite-plugin-dts": "^
|
|
68
|
+
"vite-plugin-dts": "^5.0.2"
|
|
63
69
|
}
|
|
64
70
|
}
|
|
@@ -121,9 +121,15 @@ const loadStatus = ref('idle');
|
|
|
121
121
|
const imageWidth = ref<number | null>(null);
|
|
122
122
|
const imageHeight = ref<number | null>(null);
|
|
123
123
|
|
|
124
|
+
// 本地图片上传状态
|
|
125
|
+
const hasLocalImage = ref(false);
|
|
126
|
+
const localImageUrl = ref('');
|
|
127
|
+
const isDragOver = ref(false);
|
|
128
|
+
|
|
124
129
|
// UI 状态
|
|
125
130
|
const showBrushPanel = ref(false);
|
|
126
131
|
const brushButtonRect = ref({ x: 0, y: 0, width: 0, height: 0 });
|
|
132
|
+
const showTools = computed(() => loadStatus.value === 'success');
|
|
127
133
|
|
|
128
134
|
// 笔刷样式
|
|
129
135
|
const localBrushStyle = ref<BrushStyle>({ ...DEFAULT_BRUSH_STYLE });
|
|
@@ -136,6 +142,9 @@ const localBrushStyle = ref<BrushStyle>({ ...DEFAULT_BRUSH_STYLE });
|
|
|
136
142
|
| pointermove | handlePointerMove | 笔刷绘制/鼠标追踪 |
|
|
137
143
|
| pointerup | handlePointerUp | 完成笔刷绘制 |
|
|
138
144
|
| keydown | handleKeyDown | 热键处理(tinykeys) |
|
|
145
|
+
| dragover | handleDragOver | 拖拽文件时高亮边框 |
|
|
146
|
+
| dragleave | handleDragLeave | 拖拽离开时取消高亮 |
|
|
147
|
+
| drop | handleDrop | 拖拽文件时处理上传 |
|
|
139
148
|
|
|
140
149
|
**对外 API**:
|
|
141
150
|
```typescript
|
|
@@ -344,12 +353,46 @@ const switchToSelect = () => {
|
|
|
344
353
|
|
|
345
354
|
| 属性名 | 类型 | 默认值 | 说明 |
|
|
346
355
|
|--------|------|--------|------|
|
|
347
|
-
| imageSource | { id, url } |
|
|
356
|
+
| imageSource | { id?: string, url: string } \| null | null | 图片源配置(可选,未提供时显示本地上传界面) |
|
|
348
357
|
| pointStyle | Partial\<PointStyle\> | DEFAULT_POINT_STYLE | 点标注样式 |
|
|
349
358
|
| brushStyle | Partial\<BrushStyle\> | DEFAULT_BRUSH_STYLE | 笔刷样式 |
|
|
350
359
|
| options | { maskExportFormat, maskExportForeground } | - | 导出配置 |
|
|
351
360
|
|
|
352
|
-
### 7.3
|
|
361
|
+
### 7.3 图片切换逻辑
|
|
362
|
+
|
|
363
|
+
**图片加载策略**:
|
|
364
|
+
1. 优先使用本地上传的图片(hasLocalImage 为 true)
|
|
365
|
+
2. 其次使用 props.imageSource.url
|
|
366
|
+
3. 无图片时显示上传界面
|
|
367
|
+
|
|
368
|
+
**Props 监听**:
|
|
369
|
+
```typescript
|
|
370
|
+
watch(
|
|
371
|
+
() => props.imageSource?.url,
|
|
372
|
+
(newUrl, oldUrl) => {
|
|
373
|
+
if (newUrl) {
|
|
374
|
+
// 有新图片 URL,加载新图片
|
|
375
|
+
hasLocalImage.value = false;
|
|
376
|
+
localImageUrl.value = '';
|
|
377
|
+
loadImage(newUrl);
|
|
378
|
+
} else if (oldUrl && !newUrl) {
|
|
379
|
+
// 图片 URL 变空,清空画布回到上传状态
|
|
380
|
+
hasLocalImage.value = false;
|
|
381
|
+
localImageUrl.value = '';
|
|
382
|
+
clearAllAnnotationsAndBrush();
|
|
383
|
+
if (imageBox) {
|
|
384
|
+
contentLayer.clear();
|
|
385
|
+
imageBox.destroy();
|
|
386
|
+
imageBox = null;
|
|
387
|
+
}
|
|
388
|
+
loadStatus.value = 'idle';
|
|
389
|
+
}
|
|
390
|
+
},
|
|
391
|
+
{ immediate: true }
|
|
392
|
+
);
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
### 7.4 Events
|
|
353
396
|
|
|
354
397
|
| 事件名 | 参数 | 说明 |
|
|
355
398
|
|--------|------|------|
|
|
@@ -396,6 +439,6 @@ const switchToSelect = () => {
|
|
|
396
439
|
|
|
397
440
|
---
|
|
398
441
|
|
|
399
|
-
**文档版本**:2.
|
|
442
|
+
**文档版本**:2.1
|
|
400
443
|
**创建日期**:2026-04-28
|
|
401
|
-
**最后更新**:2026-05-
|
|
444
|
+
**最后更新**:2026-05-08
|
|
@@ -95,17 +95,27 @@
|
|
|
95
95
|
| 38 | 暴露画布控制方法 | zoomIn / zoomOut / resetZoom | 画布缩放 | ✅ |
|
|
96
96
|
| 39 | 暴露导出导入方法 | exportCanvasJSON / importCanvasJSON / exportCOCO / exportYOLO | 导出导入逻辑 | ✅ |
|
|
97
97
|
|
|
98
|
-
### 阶段八:测试与优化(P3
|
|
98
|
+
### 阶段八:测试与优化(P3)✅ 已完成
|
|
99
|
+
|
|
100
|
+
| 序号 | 任务 | 描述 | 依赖 | 状态 |
|
|
101
|
+
|------|------|------|------|------|
|
|
102
|
+
| 40 | 本地图片上传功能 | 支持点击选择和拖拽上传本地图片 | 画布初始化 | ✅ |
|
|
103
|
+
| 41 | props 图片切换优化 | 监听 props.imageSource.url 变化,自动切换图片 | 图片加载逻辑 | ✅ |
|
|
104
|
+
| 42 | 图片清空功能 | props.imageSource.url 为空时,清空画布回到上传状态 | 图片加载逻辑 | ✅ |
|
|
105
|
+
| 43 | 无图片界面优化 | 无图片时隐藏工具栏和缩放控制器,只显示上传界面 | UI 层 | ✅ |
|
|
106
|
+
| 44 | 上传区域样式优化 | 上传区域占满整个画布,更大气简洁 | UI 层 | ✅ |
|
|
107
|
+
|
|
108
|
+
### 阶段九:测试与优化(P3)⏳ 待完成
|
|
99
109
|
|
|
100
110
|
| 序号 | 任务 | 描述 | 依赖 | 优先级 |
|
|
101
111
|
|------|------|------|------|------|
|
|
102
|
-
|
|
|
103
|
-
|
|
|
104
|
-
|
|
|
105
|
-
|
|
|
106
|
-
|
|
|
107
|
-
|
|
|
108
|
-
|
|
|
112
|
+
| 45 | 坐标转换测试 | 像素坐标 ↔ 归一化坐标正确性校验 | 点标注数据 | P1 |
|
|
113
|
+
| 46 | 标签编辑测试 | 空值校验、编辑状态正确性 | 标签编辑逻辑 | P1 |
|
|
114
|
+
| 47 | 撤销/重做完整性测试 | 确保所有操作可正确撤销/重做 | 所有命令 | P1 |
|
|
115
|
+
| 48 | 性能优化 | Canvas 渲染优化、高频事件防抖 | 所有功能 | P2 |
|
|
116
|
+
| 49 | 边界处理 | 极端坐标、空数据、异常输入的容错 | 所有功能 | P2 |
|
|
117
|
+
| 50 | 跨浏览器测试 | 确保在各浏览器正常工作 | 所有功能 | P3 |
|
|
118
|
+
| 51 | 文档完善 | 更新 README、API 文档、添加示例 | 所有功能 | P2 |
|
|
109
119
|
|
|
110
120
|
---
|
|
111
121
|
|
|
@@ -165,14 +175,21 @@
|
|
|
165
175
|
├── 38. 画布控制方法
|
|
166
176
|
└── 39. 导出导入方法
|
|
167
177
|
↓
|
|
168
|
-
|
|
169
|
-
├── 40.
|
|
170
|
-
├── 41.
|
|
171
|
-
├── 42.
|
|
172
|
-
├── 43.
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
178
|
+
阶段八:本地图片上传与优化
|
|
179
|
+
├── 40. 本地图片上传功能
|
|
180
|
+
├── 41. props 图片切换优化
|
|
181
|
+
├── 42. 图片清空功能
|
|
182
|
+
├── 43. 无图片界面优化
|
|
183
|
+
└── 44. 上传区域样式优化
|
|
184
|
+
↓
|
|
185
|
+
阶段九:测试与优化
|
|
186
|
+
├── 45. 坐标转换测试
|
|
187
|
+
├── 46. 标签编辑测试
|
|
188
|
+
├── 47. 撤销/重做完整性测试
|
|
189
|
+
├── 48. 性能优化
|
|
190
|
+
├── 49. 边界处理
|
|
191
|
+
├── 50. 跨浏览器测试
|
|
192
|
+
└── 51. 文档完善
|
|
176
193
|
```
|
|
177
194
|
|
|
178
195
|
---
|
|
@@ -187,10 +204,11 @@
|
|
|
187
204
|
| M4 | 交互完善 | 工具栏、画布控制完成 | ✅ 完成 |
|
|
188
205
|
| M5 | 数据导出 | 所有导出格式支持完成 | ✅ 完成 |
|
|
189
206
|
| M6 | API 暴露 | 所有对外 API 完成 | ✅ 完成 |
|
|
190
|
-
| M7 |
|
|
207
|
+
| M7 | 本地图片上传 | 支持本地上传、拖拽、props 图片切换 | ✅ 完成 |
|
|
208
|
+
| M8 | 测试优化 | 测试通过、性能优化完成 | ⏳ 进行中 |
|
|
191
209
|
|
|
192
210
|
---
|
|
193
211
|
|
|
194
|
-
**文档版本**:2.
|
|
212
|
+
**文档版本**:2.1
|
|
195
213
|
**创建日期**:2026-04-28
|
|
196
|
-
**最后更新**:2026-05-
|
|
214
|
+
**最后更新**:2026-05-08
|
|
@@ -151,12 +151,22 @@
|
|
|
151
151
|
| P6-003 | 支持图片自适应画布 | ⭐⭐⭐ |
|
|
152
152
|
| P6-004 | 显示当前缩放比例 | ⭐⭐ |
|
|
153
153
|
|
|
154
|
-
### 2.7
|
|
154
|
+
### 2.7 本地图片上传功能
|
|
155
155
|
|
|
156
156
|
| 需求编号 | 功能描述 | 优先级 |
|
|
157
157
|
|----------|----------|--------|
|
|
158
|
-
| P7-001 |
|
|
159
|
-
| P7-002 |
|
|
158
|
+
| P7-001 | imageSource 未提供时,显示本地上传界面 | ⭐⭐⭐ |
|
|
159
|
+
| P7-002 | 支持点击选择本地图片文件 | ⭐⭐⭐ |
|
|
160
|
+
| P7-003 | 支持拖拽图片文件到画布区域上传 | ⭐⭐⭐ |
|
|
161
|
+
| P7-004 | 本地图片上传后,自动加载到画布 | ⭐⭐⭐ |
|
|
162
|
+
| P7-005 | 上传区域样式:占满整个画布,虚线边框,拖拽时有视觉反馈 | ⭐⭐⭐ |
|
|
163
|
+
|
|
164
|
+
### 2.8 清除所有功能
|
|
165
|
+
|
|
166
|
+
| 需求编号 | 功能描述 | 优先级 |
|
|
167
|
+
|----------|----------|--------|
|
|
168
|
+
| P8-001 | select 模式下未选中任何标注点时,按 Delete 清除所有标注和笔刷 | ⭐⭐⭐ |
|
|
169
|
+
| P8-002 | 清除前显示确认对话框 | ⭐⭐⭐ |
|
|
160
170
|
|
|
161
171
|
---
|
|
162
172
|
|
|
@@ -264,7 +274,7 @@ interface YOLOExport {
|
|
|
264
274
|
|
|
265
275
|
| 属性名 | 类型 | 默认值 | 说明 |
|
|
266
276
|
|--------|------|--------|------|
|
|
267
|
-
| imageSource | { id?: string, url: string } |
|
|
277
|
+
| imageSource | { id?: string, url: string } \| null | null | 图片源配置(可选,未提供时显示本地上传界面) |
|
|
268
278
|
|
|
269
279
|
### 4.2 点标注样式配置
|
|
270
280
|
|
|
@@ -352,7 +362,16 @@ interface YOLOExport {
|
|
|
352
362
|
3. **清除**:点击删除按钮或按 Delete 键清除所有涂抹
|
|
353
363
|
4. **配置**:点击笔刷工具按钮打开配置面板调整样式
|
|
354
364
|
|
|
355
|
-
### 5.5
|
|
365
|
+
### 5.5 本地图片上传交互
|
|
366
|
+
|
|
367
|
+
1. **无图片状态**:imageSource 未提供时,整个画布区域显示上传界面
|
|
368
|
+
2. **点击上传**:点击"选择图片"按钮打开文件选择器
|
|
369
|
+
3. **拖拽上传**:拖拽图片文件到画布区域,显示高亮边框反馈
|
|
370
|
+
4. **文件选择**:选择本地图片后,自动加载到画布
|
|
371
|
+
5. **图片切换**:通过 props.imageSource.url 变化时,自动切换图片并清空画布
|
|
372
|
+
6. **图片清空**:props.imageSource.url 为空时,清空画布回到上传状态
|
|
373
|
+
|
|
374
|
+
### 5.6 清除所有交互
|
|
356
375
|
|
|
357
376
|
1. **条件**:select 模式下未选中任何标注点
|
|
358
377
|
2. **触发**:点击删除按钮或按 Delete 键
|
|
@@ -512,6 +531,6 @@ PNG 或 JPG 格式图片,涂抹区域为前景色,其余为透明(PNG)
|
|
|
512
531
|
|
|
513
532
|
---
|
|
514
533
|
|
|
515
|
-
**文档版本**:2.
|
|
516
|
-
**创建日期**:2024-04-28
|
|
517
|
-
**最后更新**:
|
|
534
|
+
**文档版本**:2.1
|
|
535
|
+
**创建日期**:2024-04-28
|
|
536
|
+
**最后更新**:2026-05-08
|
package/src/App.vue
CHANGED
|
@@ -116,11 +116,14 @@ import { ref, computed } from 'vue'
|
|
|
116
116
|
import PointAnnotation from './components/PointAnnotation.vue'
|
|
117
117
|
|
|
118
118
|
// 图片URL
|
|
119
|
-
const imageUrl = ref('
|
|
120
|
-
const imageSource = computed(() =>
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
119
|
+
const imageUrl = ref('')
|
|
120
|
+
const imageSource = computed(() => {
|
|
121
|
+
if (!imageUrl.value) return undefined
|
|
122
|
+
return {
|
|
123
|
+
id: 'test-image',
|
|
124
|
+
url: imageUrl.value
|
|
125
|
+
}
|
|
126
|
+
})
|
|
124
127
|
|
|
125
128
|
// 编辑器选项
|
|
126
129
|
const editorOptions = ref({
|