luo-image-annotator 0.0.3 → 0.0.4

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 CHANGED
@@ -102,7 +102,7 @@ npm publish --otp=你的恢复码字符串
102
102
  npm install luo-image-annotator
103
103
  ```
104
104
 
105
- ### 2. 全局引入 (推荐)
105
+ ### 2. 引入样式与组件
106
106
 
107
107
  在 `main.ts` 或 `main.js` 中引入样式和组件:
108
108
 
@@ -124,26 +124,75 @@ app.component('BatchAnnotator', BatchAnnotator)
124
124
  app.mount('#app')
125
125
  ```
126
126
 
127
- ### 3. 局部引入
127
+ ### 3. 平台嵌入流程(推荐)
128
+
129
+ #### 第一步:平台下发任务与图片
130
+
131
+ 平台侧准备以下数据并传入组件:
132
+
133
+ - `batchImages`:图片与历史标注
134
+ - `labels`:标签体系
135
+ - `session`:`projectId/taskId/userId/role`
136
+ - `requestId`:一次页面会话追踪 ID
137
+
138
+ #### 第二步:监听标准化事件
139
+
140
+ 组件会发出两类事件:
141
+
142
+ - 兼容事件:`annotationChange`、`batchChange`、`labelChange`
143
+ - 标准事件:`annotation:add`、`annotation:update`、`annotation:delete`、`annotation:select`、`prediction:*`、`review:action`、`qa:issue`
144
+
145
+ 平台建议仅消费标准事件做业务编排,兼容事件用于平滑迁移。
146
+
147
+ #### 第三步:自动标注闭环
148
+
149
+ 1. 用户点击自动标注按钮,调用组件实例 `requestPrediction(threshold)`
150
+ 2. 组件触发 `prediction:request` 事件
151
+ 3. 平台调用算法服务,拿到候选结果后调用组件实例 `loadPredictionCandidates(candidates)`
152
+ 4. 标注员筛选后调用 `applyPredictions(ids)` 或 `rejectPredictions(ids, reason)`
153
+ 5. 平台监听 `prediction:apply` / `prediction:reject` 持久化结果
154
+
155
+ #### 第四步:审阅质检闭环
156
+
157
+ 1. 审阅员调用 `reviewAction('pass'|'reject'|'revise', annotationIds, comment)`
158
+ 2. 组件更新标注 `reviewStatus` 并触发 `review:action`
159
+ 3. 质检员调用 `reportQaIssue(issueType, annotationIds, detail)` 触发 `qa:issue`
160
+ 4. 平台决定状态流转与权限校验
161
+
162
+ ### 4. 局部引入示例
128
163
 
129
164
  你也可以在单个 `.vue` 文件中局部引入:
130
165
 
131
166
  ```vue
132
167
  <template>
133
168
  <div style="height: 100vh;">
134
- <BatchAnnotator
135
- :images="images"
136
- :labels="labels"
137
- @export="handleExport"
169
+ <ImageAnnotator
170
+ ref="annotatorRef"
171
+ :batchImages="images"
172
+ :labels="labels"
173
+ :annotationTypes="['rectangle', 'polygon', 'point', 'rotatedRect']"
174
+ :session="session"
175
+ :requestId="requestId"
176
+ :predictionCandidates="predictionCandidates"
177
+ @annotation:add="onAnnotationAdd"
178
+ @annotation:update="onAnnotationUpdate"
179
+ @annotation:delete="onAnnotationDelete"
180
+ @prediction:request="onPredictionRequest"
181
+ @prediction:apply="onPredictionApply"
182
+ @prediction:reject="onPredictionReject"
183
+ @review:action="onReviewAction"
184
+ @qa:issue="onQaIssue"
138
185
  />
139
186
  </div>
140
187
  </template>
141
188
 
142
189
  <script setup lang="ts">
143
190
  import { ref } from 'vue';
144
- import { BatchAnnotator } from 'luo-image-annotator';
191
+ import { ImageAnnotator } from 'luo-image-annotator';
145
192
  import 'luo-image-annotator/dist/style.css'; // 别忘了引入样式
146
193
 
194
+ const annotatorRef = ref<InstanceType<typeof ImageAnnotator> | null>(null);
195
+
147
196
  // 定义标签
148
197
  const labels = [
149
198
  { id: '1', name: 'Person', color: '#FF0000', visible: true },
@@ -159,9 +208,59 @@ const images = ref([
159
208
  // ... 更多图片
160
209
  ]);
161
210
 
162
- // 处理导出事件
163
- const handleExport = (data) => {
164
- console.log('导出的标注数据:', data);
211
+ const session = {
212
+ projectId: 'proj-1',
213
+ taskId: 'task-1001',
214
+ userId: 'u-01',
215
+ role: 'annotator'
216
+ };
217
+
218
+ const requestId = 'req-20260318-001';
219
+ const predictionCandidates = ref([]);
220
+
221
+ const onAnnotationAdd = (payload) => {
222
+ console.log('新增标注', payload);
223
+ };
224
+
225
+ const onAnnotationUpdate = (payload) => {
226
+ console.log('更新标注', payload);
227
+ };
228
+
229
+ const onAnnotationDelete = (payload) => {
230
+ console.log('删除标注', payload);
231
+ };
232
+
233
+ const onPredictionRequest = async () => {
234
+ const fakeCandidates = [
235
+ {
236
+ id: 'pred-1',
237
+ confidence: 0.92,
238
+ modelRunId: 'run-1',
239
+ annotation: {
240
+ id: 'a-pred-1',
241
+ type: 'rectangle',
242
+ label: 'Person',
243
+ coordinates: { x1: 120, y1: 80, x2: 260, y2: 320 }
244
+ }
245
+ }
246
+ ];
247
+ annotatorRef.value?.loadPredictionCandidates(fakeCandidates);
248
+ };
249
+
250
+ const onPredictionApply = (payload) => {
251
+ console.log('接受预测', payload);
252
+ };
253
+
254
+ const onPredictionReject = (payload) => {
255
+ console.log('拒绝预测', payload);
256
+ };
257
+
258
+ const onReviewAction = (payload) => {
259
+ console.log('审阅动作', payload);
260
+ };
261
+
262
+ const onQaIssue = (payload) => {
263
+ console.log('质检问题', payload);
165
264
  };
166
265
  </script>
167
266
  ```
@@ -182,6 +281,55 @@ const handleExport = (data) => {
182
281
  | `export` | `data: Array` | 点击“导出”按钮时触发,返回最新的图片和标注数据 |
183
282
  | `update:images` | `data: Array` | 当标注数据发生变化时触发,支持 `v-model:images` |
184
283
 
284
+ ### ImageAnnotator Props(新增能力)
285
+
286
+ | 属性名 | 类型 | 必填 | 描述 |
287
+ | --- | --- | --- | --- |
288
+ | `batchImages` | `Array` | 否 | 批量图片与标注 |
289
+ | `image` | `Object` | 否 | 单图模式输入,结构 `{ id, url, meta }` |
290
+ | `labels` | `Array` | 否 | 标签定义 |
291
+ | `annotationTypes` | `Array` | 否 | 工具类型列表 |
292
+ | `predictionCandidates` | `Array` | 否 | 自动标注候选结果 |
293
+ | `reviewMode` | `string` | 否 | `off/reviewer/qa` |
294
+ | `session` | `Object` | 否 | 会话上下文 `{ projectId, taskId, userId, role }` |
295
+ | `requestId` | `string` | 否 | 平台链路追踪 ID |
296
+
297
+ ### ImageAnnotator 标准事件
298
+
299
+ | 事件名 | 描述 |
300
+ | --- | --- |
301
+ | `ready` | 组件初始化完成 |
302
+ | `tool:change` | 工具切换 |
303
+ | `viewport:change` | 画布缩放/视口变化 |
304
+ | `annotation:add` | 新增标注 |
305
+ | `annotation:update` | 更新标注 |
306
+ | `annotation:delete` | 删除标注 |
307
+ | `annotation:select` | 选中标注 |
308
+ | `prediction:request` | 请求自动标注 |
309
+ | `prediction:loaded` | 自动标注候选已加载 |
310
+ | `prediction:apply` | 接受候选结果 |
311
+ | `prediction:reject` | 拒绝候选结果 |
312
+ | `review:action` | 审阅动作 |
313
+ | `qa:issue` | 质检问题上报 |
314
+ | `error` | 错误事件 |
315
+
316
+ ### ImageAnnotator 实例方法
317
+
318
+ | 方法名 | 参数 | 描述 |
319
+ | --- | --- | --- |
320
+ | `jumpTo` | `(index: number)` | 批量模式跳转指定图片 |
321
+ | `setImage` | `(image, annotations?)` | 设置单图及标注 |
322
+ | `setAnnotations` | `(annotations)` | 覆盖当前标注 |
323
+ | `getAnnotations` | `()` | 获取当前标注 |
324
+ | `selectTool` | `(tool)` | 切换工具 |
325
+ | `requestPrediction` | `(threshold?)` | 触发自动标注请求事件 |
326
+ | `loadPredictionCandidates` | `(candidates)` | 加载自动标注候选 |
327
+ | `applyPredictions` | `(candidateIds, threshold?)` | 批量接受候选 |
328
+ | `rejectPredictions` | `(candidateIds, reason?)` | 批量拒绝候选 |
329
+ | `reviewAction` | `(action, annotationIds, comment?)` | 执行审阅动作 |
330
+ | `reportQaIssue` | `(issueType, annotationIds, detail?)` | 上报质检问题 |
331
+ | `exportAnnotations` | `(format?)` | 导出当前结果 |
332
+
185
333
  ## 许可证
186
334
 
187
335
  MIT
@@ -1 +1 @@
1
- .svg-icon[data-v-3928607b]{display:inline-flex;align-items:center;justify-content:center;width:1em;height:1em;fill:currentColor;font-style:normal;line-height:0;text-align:center;text-transform:none;vertical-align:-.125em;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.svg-icon[data-v-3928607b] svg{width:1em;height:1em;fill:currentColor}.size-small[data-v-3928607b]{font-size:12px}.size-large[data-v-3928607b]{font-size:20px}.annotation-container[data-v-fe1b36c0]{display:flex;height:100%;width:100%;border:1px solid #e0e0e0;background:#f5f5f5;overflow:hidden}.left-sidebar[data-v-fe1b36c0]{width:50px;background:#fff;border-right:1px solid #e0e0e0;display:flex;flex-direction:column;align-items:center;padding:8px 0;gap:8px;z-index:10}.tool-btn[data-v-fe1b36c0]{width:36px;height:36px;display:flex;align-items:center;justify-content:center;border:1px solid transparent;border-radius:4px;cursor:pointer;-webkit-user-select:none;user-select:none;font-size:18px;transition:all .2s}.tool-btn[data-v-fe1b36c0]:hover{background:#f0f0f0}.tool-btn.active[data-v-fe1b36c0]{background:#e3f2fd;border-color:#2196f3;color:#2196f3}.divider[data-v-fe1b36c0]{width:80%;height:1px;background:#ddd;margin:4px 0}.center-area[data-v-fe1b36c0]{flex:1;display:flex;flex-direction:column;position:relative;overflow:hidden}.top-bar[data-v-fe1b36c0]{height:50px;background:#fff;border-bottom:1px solid #e0e0e0;display:flex;align-items:center;padding:0 16px}.label-selector[data-v-fe1b36c0]{display:flex;align-items:center;gap:12px;width:100%;overflow-x:auto}.label-text[data-v-fe1b36c0]{font-size:14px;font-weight:700;color:#555;white-space:nowrap}.tags-row[data-v-fe1b36c0]{display:flex;gap:8px}.tag-chip[data-v-fe1b36c0]{padding:4px 12px;border-radius:16px;font-size:12px;color:#fff;cursor:pointer;border:2px solid transparent;opacity:.7;transition:all .2s;white-space:nowrap}.tag-chip.active[data-v-fe1b36c0]{opacity:1;transform:scale(1.05);box-shadow:0 2px 4px #0003}.canvas-wrapper[data-v-fe1b36c0]{flex:1;background:#333;position:relative;overflow:hidden}.batch-nav[data-v-fe1b36c0]{height:48px;background:#fff;border-top:1px solid #e0e0e0;display:flex;justify-content:center;align-items:center;gap:16px}.batch-nav button[data-v-fe1b36c0]{padding:6px 16px;background:#f5f5f5;border:1px solid #ddd;border-radius:4px;cursor:pointer}.batch-nav button[data-v-fe1b36c0]:hover:not(:disabled){background:#e0e0e0}.right-sidebar[data-v-fe1b36c0]{width:250px;background:#fff;border-left:1px solid #e0e0e0;display:flex;flex-direction:column;z-index:10}.sidebar-header[data-v-fe1b36c0]{padding:16px;border-bottom:1px solid #eee;display:flex;justify-content:space-between;align-items:center}.sidebar-header h3[data-v-fe1b36c0]{margin:0;font-size:16px}.add-btn[data-v-fe1b36c0]{padding:4px 8px;background:#2196f3;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:12px}.label-list[data-v-fe1b36c0]{flex:1;overflow-y:auto;padding:8px}.label-item[data-v-fe1b36c0]{margin-bottom:8px;padding:8px;background:#f9f9f9;border-radius:4px;border:1px solid #eee}.label-row[data-v-fe1b36c0]{display:flex;align-items:center;gap:8px}.eye-icon[data-v-fe1b36c0],.delete-icon[data-v-fe1b36c0]{cursor:pointer;font-size:16px;-webkit-user-select:none;user-select:none;opacity:.7}.eye-icon[data-v-fe1b36c0]:hover,.delete-icon[data-v-fe1b36c0]:hover{opacity:1}.color-picker[data-v-fe1b36c0]{width:24px;height:24px;padding:0;border:none;cursor:pointer;background:none}.name-input[data-v-fe1b36c0]{flex:1;border:1px solid transparent;background:transparent;font-size:14px;padding:2px 4px}.name-input[data-v-fe1b36c0]:focus{border-color:#2196f3;background:#fff;outline:none}.color-wrapper[data-v-fe1b36c0]{width:16px;height:16px;border-radius:50%;cursor:pointer;border:1px solid rgba(0,0,0,.1);flex-shrink:0}.label-name[data-v-fe1b36c0]{flex:1;font-size:14px;color:#333;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.action-icon[data-v-fe1b36c0]{cursor:pointer;font-size:16px;-webkit-user-select:none;user-select:none;width:24px;text-align:center;opacity:.6}.action-icon[data-v-fe1b36c0]:hover{opacity:1}.more-actions[data-v-fe1b36c0]{position:relative;display:flex;justify-content:center;align-items:center}.more-actions .delete-btn[data-v-fe1b36c0]{display:none;font-size:14px}.more-actions:hover .dots[data-v-fe1b36c0]{display:none}.more-actions:hover .delete-btn[data-v-fe1b36c0]{display:inline-block;color:#f44336}.modal-overlay[data-v-fe1b36c0]{position:fixed;top:0;left:0;width:100%;height:100%;background:#00000080;display:flex;justify-content:center;align-items:center;z-index:1000}.modal-content[data-v-fe1b36c0]{background:#fff;padding:20px;border-radius:8px;width:300px;box-shadow:0 4px 12px #00000026}.modal-content h3[data-v-fe1b36c0]{margin:0 0 16px;font-size:18px;color:#333}.form-group[data-v-fe1b36c0]{margin-bottom:16px}.form-group label[data-v-fe1b36c0]{display:block;margin-bottom:8px;font-size:14px;color:#666}.modal-input[data-v-fe1b36c0]{width:100%;padding:8px;border:1px solid #ddd;border-radius:4px;font-size:14px;box-sizing:border-box}.color-input-wrapper[data-v-fe1b36c0]{display:flex;align-items:center;gap:8px}.modal-color-picker[data-v-fe1b36c0]{width:40px;height:30px;padding:0;border:none;background:none;cursor:pointer}.modal-actions[data-v-fe1b36c0]{display:flex;justify-content:flex-end;gap:12px;margin-top:24px}.cancel-btn[data-v-fe1b36c0]{padding:6px 16px;background:#f5f5f5;border:1px solid #ddd;border-radius:4px;cursor:pointer;color:#666}.confirm-btn[data-v-fe1b36c0]{padding:6px 16px;background:#2196f3;border:none;border-radius:4px;cursor:pointer;color:#fff}.cancel-btn[data-v-fe1b36c0]:hover{background:#e0e0e0}.confirm-btn[data-v-fe1b36c0]:hover{background:#1976d2}.thumbnail-wrapper[data-v-78bcbe0c]{position:relative;width:100%;height:100%;overflow:hidden;border-radius:4px;background:#f0f0f0}.thumbnail-image[data-v-78bcbe0c]{width:100%;height:100%;object-fit:cover;display:block}.annotation-overlay[data-v-78bcbe0c]{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none}.loading-placeholder[data-v-78bcbe0c]{position:absolute;top:0;left:0;width:100%;height:100%;display:flex;align-items:center;justify-content:center;color:#999;font-size:12px}.anno-label[data-v-78bcbe0c]{paint-order:stroke;stroke:#fff;stroke-width:2px;stroke-linecap:round;stroke-linejoin:round}.batch-annotator[data-v-87f3e002]{width:100%;height:100vh;display:flex;flex-direction:column;background:#f5f5f5}.gallery-view[data-v-87f3e002]{flex:1;display:flex;flex-direction:column;overflow:hidden;padding:20px;position:relative}.gallery-header[data-v-87f3e002]{margin-bottom:20px;display:flex;justify-content:space-between;align-items:center;flex-shrink:0}.gallery-header h3[data-v-87f3e002]{margin:0;font-size:20px;color:#333}.label-summary[data-v-87f3e002]{display:flex;gap:8px}.label-badge[data-v-87f3e002]{padding:4px 10px;border-radius:12px;color:#fff;font-size:12px;font-weight:700}.gallery-grid[data-v-87f3e002]{flex:1;display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:20px;overflow-y:auto;padding-bottom:80px}.gallery-item[data-v-87f3e002]{background:#fff;border-radius:8px;overflow:hidden;box-shadow:0 2px 8px #0000001a;cursor:pointer;transition:transform .2s,box-shadow .2s;display:flex;flex-direction:column;height:240px}.gallery-item[data-v-87f3e002]:hover{transform:translateY(-4px);box-shadow:0 4px 12px #00000026}.thumbnail-wrapper[data-v-87f3e002]{flex:1;overflow:hidden;position:relative}.img-meta[data-v-87f3e002]{padding:8px;font-size:12px;color:#666;display:flex;justify-content:space-between;background:#fff;border-top:1px solid #eee;height:32px;align-items:center}.bottom-bar[data-v-87f3e002]{position:absolute;bottom:0;left:0;width:100%;height:60px;background:#fff;border-top:1px solid #e0e0e0;display:flex;justify-content:space-between;align-items:center;padding:0 40px;box-shadow:0 -2px 10px #0000000d;z-index:100}.action-btn[data-v-87f3e002]{display:flex;align-items:center;gap:8px;padding:10px 24px;border:none;border-radius:4px;font-size:16px;cursor:pointer;transition:background .2s}.action-btn.primary[data-v-87f3e002]{background:#2196f3;color:#fff}.action-btn.primary[data-v-87f3e002]:hover{background:#1976d2}.action-btn.success[data-v-87f3e002]{background:#4caf50;color:#fff}.action-btn.success[data-v-87f3e002]:hover{background:#388e3c}.editor-view[data-v-87f3e002]{flex:1;display:flex;flex-direction:column;height:100%}.editor-header[data-v-87f3e002]{height:50px;background:#fff;border-bottom:1px solid #e0e0e0;display:flex;align-items:center;padding:0 16px;justify-content:space-between;flex-shrink:0}.header-left[data-v-87f3e002]{display:flex;align-items:center;gap:16px}.back-btn[data-v-87f3e002]{display:flex;align-items:center;gap:4px;background:transparent;border:1px solid #ddd;padding:6px 12px;border-radius:4px;cursor:pointer;font-size:14px;color:#666}.back-btn[data-v-87f3e002]:hover{background:#f5f5f5;color:#333}.editor-content[data-v-87f3e002]{flex:1;overflow:hidden;position:relative}
1
+ .svg-icon[data-v-3928607b]{display:inline-flex;align-items:center;justify-content:center;width:1em;height:1em;fill:currentColor;font-style:normal;line-height:0;text-align:center;text-transform:none;vertical-align:-.125em;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.svg-icon[data-v-3928607b] svg{width:1em;height:1em;fill:currentColor}.size-small[data-v-3928607b]{font-size:12px}.size-large[data-v-3928607b]{font-size:20px}.annotation-container[data-v-7b013204]{display:flex;height:100%;width:100%;border:1px solid #e0e0e0;background:#f5f5f5;overflow:hidden}.left-sidebar[data-v-7b013204]{width:50px;background:#fff;border-right:1px solid #e0e0e0;display:flex;flex-direction:column;align-items:center;padding:8px 0;gap:8px;z-index:10}.tool-btn[data-v-7b013204]{width:36px;height:36px;display:flex;align-items:center;justify-content:center;border:1px solid transparent;border-radius:4px;cursor:pointer;-webkit-user-select:none;user-select:none;font-size:18px;transition:all .2s}.tool-btn[data-v-7b013204]:hover{background:#f0f0f0}.tool-btn.active[data-v-7b013204]{background:#e3f2fd;border-color:#2196f3;color:#2196f3}.divider[data-v-7b013204]{width:80%;height:1px;background:#ddd;margin:4px 0}.center-area[data-v-7b013204]{flex:1;display:flex;flex-direction:column;position:relative;overflow:hidden}.top-bar[data-v-7b013204]{height:50px;background:#fff;border-bottom:1px solid #e0e0e0;display:flex;align-items:center;padding:0 16px}.label-selector[data-v-7b013204]{display:flex;align-items:center;gap:12px;width:100%;overflow-x:auto}.label-text[data-v-7b013204]{font-size:14px;font-weight:700;color:#555;white-space:nowrap}.tags-row[data-v-7b013204]{display:flex;gap:8px}.tag-chip[data-v-7b013204]{padding:4px 12px;border-radius:16px;font-size:12px;color:#fff;cursor:pointer;border:2px solid transparent;opacity:.7;transition:all .2s;white-space:nowrap}.tag-chip.active[data-v-7b013204]{opacity:1;transform:scale(1.05);box-shadow:0 2px 4px #0003}.canvas-wrapper[data-v-7b013204]{flex:1;background:#333;position:relative;overflow:hidden}.batch-nav[data-v-7b013204]{height:48px;background:#fff;border-top:1px solid #e0e0e0;display:flex;justify-content:center;align-items:center;gap:16px}.batch-nav button[data-v-7b013204]{padding:6px 16px;background:#f5f5f5;border:1px solid #ddd;border-radius:4px;cursor:pointer}.batch-nav button[data-v-7b013204]:hover:not(:disabled){background:#e0e0e0}.right-sidebar[data-v-7b013204]{width:250px;background:#fff;border-left:1px solid #e0e0e0;display:flex;flex-direction:column;z-index:10}.sidebar-header[data-v-7b013204]{padding:16px;border-bottom:1px solid #eee;display:flex;justify-content:space-between;align-items:center}.sidebar-header h3[data-v-7b013204]{margin:0;font-size:16px}.add-btn[data-v-7b013204]{padding:4px 8px;background:#2196f3;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:12px}.label-list[data-v-7b013204]{flex:1;overflow-y:auto;padding:8px}.label-item[data-v-7b013204]{margin-bottom:8px;padding:8px;background:#f9f9f9;border-radius:4px;border:1px solid #eee}.label-row[data-v-7b013204]{display:flex;align-items:center;gap:8px}.eye-icon[data-v-7b013204],.delete-icon[data-v-7b013204]{cursor:pointer;font-size:16px;-webkit-user-select:none;user-select:none;opacity:.7}.eye-icon[data-v-7b013204]:hover,.delete-icon[data-v-7b013204]:hover{opacity:1}.color-picker[data-v-7b013204]{width:24px;height:24px;padding:0;border:none;cursor:pointer;background:none}.name-input[data-v-7b013204]{flex:1;border:1px solid transparent;background:transparent;font-size:14px;padding:2px 4px}.name-input[data-v-7b013204]:focus{border-color:#2196f3;background:#fff;outline:none}.color-wrapper[data-v-7b013204]{width:16px;height:16px;border-radius:50%;cursor:pointer;border:1px solid rgba(0,0,0,.1);flex-shrink:0}.label-name[data-v-7b013204]{flex:1;font-size:14px;color:#333;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.action-icon[data-v-7b013204]{cursor:pointer;font-size:16px;-webkit-user-select:none;user-select:none;width:24px;text-align:center;opacity:.6}.action-icon[data-v-7b013204]:hover{opacity:1}.more-actions[data-v-7b013204]{position:relative;display:flex;justify-content:center;align-items:center}.more-actions .delete-btn[data-v-7b013204]{display:none;font-size:14px}.more-actions:hover .dots[data-v-7b013204]{display:none}.more-actions:hover .delete-btn[data-v-7b013204]{display:inline-block;color:#f44336}.modal-overlay[data-v-7b013204]{position:fixed;top:0;left:0;width:100%;height:100%;background:#00000080;display:flex;justify-content:center;align-items:center;z-index:1000}.modal-content[data-v-7b013204]{background:#fff;padding:20px;border-radius:8px;width:300px;box-shadow:0 4px 12px #00000026}.modal-content h3[data-v-7b013204]{margin:0 0 16px;font-size:18px;color:#333}.form-group[data-v-7b013204]{margin-bottom:16px}.form-group label[data-v-7b013204]{display:block;margin-bottom:8px;font-size:14px;color:#666}.modal-input[data-v-7b013204]{width:100%;padding:8px;border:1px solid #ddd;border-radius:4px;font-size:14px;box-sizing:border-box}.color-input-wrapper[data-v-7b013204]{display:flex;align-items:center;gap:8px}.modal-color-picker[data-v-7b013204]{width:40px;height:30px;padding:0;border:none;background:none;cursor:pointer}.modal-actions[data-v-7b013204]{display:flex;justify-content:flex-end;gap:12px;margin-top:24px}.cancel-btn[data-v-7b013204]{padding:6px 16px;background:#f5f5f5;border:1px solid #ddd;border-radius:4px;cursor:pointer;color:#666}.confirm-btn[data-v-7b013204]{padding:6px 16px;background:#2196f3;border:none;border-radius:4px;cursor:pointer;color:#fff}.cancel-btn[data-v-7b013204]:hover{background:#e0e0e0}.confirm-btn[data-v-7b013204]:hover{background:#1976d2}.thumbnail-wrapper[data-v-78bcbe0c]{position:relative;width:100%;height:100%;overflow:hidden;border-radius:4px;background:#f0f0f0}.thumbnail-image[data-v-78bcbe0c]{width:100%;height:100%;object-fit:cover;display:block}.annotation-overlay[data-v-78bcbe0c]{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none}.loading-placeholder[data-v-78bcbe0c]{position:absolute;top:0;left:0;width:100%;height:100%;display:flex;align-items:center;justify-content:center;color:#999;font-size:12px}.anno-label[data-v-78bcbe0c]{paint-order:stroke;stroke:#fff;stroke-width:2px;stroke-linecap:round;stroke-linejoin:round}.batch-annotator[data-v-87f3e002]{width:100%;height:100vh;display:flex;flex-direction:column;background:#f5f5f5}.gallery-view[data-v-87f3e002]{flex:1;display:flex;flex-direction:column;overflow:hidden;padding:20px;position:relative}.gallery-header[data-v-87f3e002]{margin-bottom:20px;display:flex;justify-content:space-between;align-items:center;flex-shrink:0}.gallery-header h3[data-v-87f3e002]{margin:0;font-size:20px;color:#333}.label-summary[data-v-87f3e002]{display:flex;gap:8px}.label-badge[data-v-87f3e002]{padding:4px 10px;border-radius:12px;color:#fff;font-size:12px;font-weight:700}.gallery-grid[data-v-87f3e002]{flex:1;display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:20px;overflow-y:auto;padding-bottom:80px}.gallery-item[data-v-87f3e002]{background:#fff;border-radius:8px;overflow:hidden;box-shadow:0 2px 8px #0000001a;cursor:pointer;transition:transform .2s,box-shadow .2s;display:flex;flex-direction:column;height:240px}.gallery-item[data-v-87f3e002]:hover{transform:translateY(-4px);box-shadow:0 4px 12px #00000026}.thumbnail-wrapper[data-v-87f3e002]{flex:1;overflow:hidden;position:relative}.img-meta[data-v-87f3e002]{padding:8px;font-size:12px;color:#666;display:flex;justify-content:space-between;background:#fff;border-top:1px solid #eee;height:32px;align-items:center}.bottom-bar[data-v-87f3e002]{position:absolute;bottom:0;left:0;width:100%;height:60px;background:#fff;border-top:1px solid #e0e0e0;display:flex;justify-content:space-between;align-items:center;padding:0 40px;box-shadow:0 -2px 10px #0000000d;z-index:100}.action-btn[data-v-87f3e002]{display:flex;align-items:center;gap:8px;padding:10px 24px;border:none;border-radius:4px;font-size:16px;cursor:pointer;transition:background .2s}.action-btn.primary[data-v-87f3e002]{background:#2196f3;color:#fff}.action-btn.primary[data-v-87f3e002]:hover{background:#1976d2}.action-btn.success[data-v-87f3e002]{background:#4caf50;color:#fff}.action-btn.success[data-v-87f3e002]:hover{background:#388e3c}.editor-view[data-v-87f3e002]{flex:1;display:flex;flex-direction:column;height:100%}.editor-header[data-v-87f3e002]{height:50px;background:#fff;border-bottom:1px solid #e0e0e0;display:flex;align-items:center;padding:0 16px;justify-content:space-between;flex-shrink:0}.header-left[data-v-87f3e002]{display:flex;align-items:center;gap:16px}.back-btn[data-v-87f3e002]{display:flex;align-items:center;gap:4px;background:transparent;border:1px solid #ddd;padding:6px 12px;border-radius:4px;cursor:pointer;font-size:14px;color:#666}.back-btn[data-v-87f3e002]:hover{background:#f5f5f5;color:#333}.editor-content[data-v-87f3e002]{flex:1;overflow:hidden;position:relative}