luo-image-annotator 0.0.2 → 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,13 +102,7 @@ npm publish --otp=你的恢复码字符串
102
102
  npm install luo-image-annotator
103
103
  ```
104
104
 
105
- 由于本组件库依赖 `element-plus` 和 `@element-plus/icons-vue`,如果你的项目中尚未安装,请同时安装它们:
106
-
107
- ```bash
108
- npm install element-plus @element-plus/icons-vue
109
- ```
110
-
111
- ### 2. 全局引入 (推荐)
105
+ ### 2. 引入样式与组件
112
106
 
113
107
  在 `main.ts` 或 `main.js` 中引入样式和组件:
114
108
 
@@ -116,51 +110,89 @@ npm install element-plus @element-plus/icons-vue
116
110
  import { createApp } from 'vue'
117
111
  import App from './App.vue'
118
112
 
119
- // 1. 引入 Element Plus (必选依赖)
120
- import ElementPlus from 'element-plus'
121
- import 'element-plus/dist/index.css'
122
- import * as ElementPlusIconsVue from '@element-plus/icons-vue'
123
-
124
- // 2. 引入本组件库样式
113
+ // 1. 引入本组件库样式
125
114
  import 'luo-image-annotator/dist/style.css'
126
115
 
127
- // 3. 引入组件
116
+ // 2. 引入组件
128
117
  import { BatchAnnotator } from 'luo-image-annotator'
129
118
 
130
119
  const app = createApp(App)
131
120
 
132
- app.use(ElementPlus)
133
- // 注册图标
134
- for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
135
- app.component(key, component)
136
- }
137
-
138
121
  // 全局注册 BatchAnnotator 组件
139
122
  app.component('BatchAnnotator', BatchAnnotator)
140
123
 
141
124
  app.mount('#app')
142
125
  ```
143
126
 
144
- ### 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. 局部引入示例
145
163
 
146
164
  你也可以在单个 `.vue` 文件中局部引入:
147
165
 
148
166
  ```vue
149
167
  <template>
150
168
  <div style="height: 100vh;">
151
- <BatchAnnotator
152
- :images="images"
153
- :labels="labels"
154
- @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"
155
185
  />
156
186
  </div>
157
187
  </template>
158
188
 
159
189
  <script setup lang="ts">
160
190
  import { ref } from 'vue';
161
- import { BatchAnnotator } from 'luo-image-annotator';
191
+ import { ImageAnnotator } from 'luo-image-annotator';
162
192
  import 'luo-image-annotator/dist/style.css'; // 别忘了引入样式
163
193
 
194
+ const annotatorRef = ref<InstanceType<typeof ImageAnnotator> | null>(null);
195
+
164
196
  // 定义标签
165
197
  const labels = [
166
198
  { id: '1', name: 'Person', color: '#FF0000', visible: true },
@@ -176,9 +208,59 @@ const images = ref([
176
208
  // ... 更多图片
177
209
  ]);
178
210
 
179
- // 处理导出事件
180
- const handleExport = (data) => {
181
- 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);
182
264
  };
183
265
  </script>
184
266
  ```
@@ -199,6 +281,55 @@ const handleExport = (data) => {
199
281
  | `export` | `data: Array` | 点击“导出”按钮时触发,返回最新的图片和标注数据 |
200
282
  | `update:images` | `data: Array` | 当标注数据发生变化时触发,支持 `v-model:images` |
201
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
+
202
333
  ## 许可证
203
334
 
204
335
  MIT
@@ -1 +1 @@
1
- .annotation-container[data-v-d3dc5a63]{display:flex;height:100%;width:100%;border:1px solid #e0e0e0;background:#f5f5f5;overflow:hidden}.left-sidebar[data-v-d3dc5a63]{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-d3dc5a63]{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-d3dc5a63]:hover{background:#f0f0f0}.tool-btn.active[data-v-d3dc5a63]{background:#e3f2fd;border-color:#2196f3;color:#2196f3}.divider[data-v-d3dc5a63]{width:80%;height:1px;background:#ddd;margin:4px 0}.center-area[data-v-d3dc5a63]{flex:1;display:flex;flex-direction:column;position:relative;overflow:hidden}.top-bar[data-v-d3dc5a63]{height:50px;background:#fff;border-bottom:1px solid #e0e0e0;display:flex;align-items:center;padding:0 16px}.label-selector[data-v-d3dc5a63]{display:flex;align-items:center;gap:12px;width:100%;overflow-x:auto}.label-text[data-v-d3dc5a63]{font-size:14px;font-weight:700;color:#555;white-space:nowrap}.tags-row[data-v-d3dc5a63]{display:flex;gap:8px}.tag-chip[data-v-d3dc5a63]{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-d3dc5a63]{opacity:1;transform:scale(1.05);box-shadow:0 2px 4px #0003}.canvas-wrapper[data-v-d3dc5a63]{flex:1;background:#333;position:relative;overflow:hidden}.batch-nav[data-v-d3dc5a63]{height:48px;background:#fff;border-top:1px solid #e0e0e0;display:flex;justify-content:center;align-items:center;gap:16px}.batch-nav button[data-v-d3dc5a63]{padding:6px 16px;background:#f5f5f5;border:1px solid #ddd;border-radius:4px;cursor:pointer}.batch-nav button[data-v-d3dc5a63]:hover:not(:disabled){background:#e0e0e0}.right-sidebar[data-v-d3dc5a63]{width:250px;background:#fff;border-left:1px solid #e0e0e0;display:flex;flex-direction:column;z-index:10}.sidebar-header[data-v-d3dc5a63]{padding:16px;border-bottom:1px solid #eee;display:flex;justify-content:space-between;align-items:center}.sidebar-header h3[data-v-d3dc5a63]{margin:0;font-size:16px}.add-btn[data-v-d3dc5a63]{padding:4px 8px;background:#2196f3;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:12px}.label-list[data-v-d3dc5a63]{flex:1;overflow-y:auto;padding:8px}.label-item[data-v-d3dc5a63]{margin-bottom:8px;padding:8px;background:#f9f9f9;border-radius:4px;border:1px solid #eee}.label-row[data-v-d3dc5a63]{display:flex;align-items:center;gap:8px}.eye-icon[data-v-d3dc5a63],.delete-icon[data-v-d3dc5a63]{cursor:pointer;font-size:16px;-webkit-user-select:none;user-select:none;opacity:.7}.eye-icon[data-v-d3dc5a63]:hover,.delete-icon[data-v-d3dc5a63]:hover{opacity:1}.color-picker[data-v-d3dc5a63]{width:24px;height:24px;padding:0;border:none;cursor:pointer;background:none}.name-input[data-v-d3dc5a63]{flex:1;border:1px solid transparent;background:transparent;font-size:14px;padding:2px 4px}.name-input[data-v-d3dc5a63]:focus{border-color:#2196f3;background:#fff;outline:none}.color-wrapper[data-v-d3dc5a63]{width:16px;height:16px;border-radius:50%;cursor:pointer;border:1px solid rgba(0,0,0,.1);flex-shrink:0}.label-name[data-v-d3dc5a63]{flex:1;font-size:14px;color:#333;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.action-icon[data-v-d3dc5a63]{cursor:pointer;font-size:16px;-webkit-user-select:none;user-select:none;width:24px;text-align:center;opacity:.6}.action-icon[data-v-d3dc5a63]:hover{opacity:1}.more-actions[data-v-d3dc5a63]{position:relative;display:flex;justify-content:center;align-items:center}.more-actions .delete-btn[data-v-d3dc5a63]{display:none;font-size:14px}.more-actions:hover .dots[data-v-d3dc5a63]{display:none}.more-actions:hover .delete-btn[data-v-d3dc5a63]{display:inline-block;color:#f44336}.modal-overlay[data-v-d3dc5a63]{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-d3dc5a63]{background:#fff;padding:20px;border-radius:8px;width:300px;box-shadow:0 4px 12px #00000026}.modal-content h3[data-v-d3dc5a63]{margin:0 0 16px;font-size:18px;color:#333}.form-group[data-v-d3dc5a63]{margin-bottom:16px}.form-group label[data-v-d3dc5a63]{display:block;margin-bottom:8px;font-size:14px;color:#666}.modal-input[data-v-d3dc5a63]{width:100%;padding:8px;border:1px solid #ddd;border-radius:4px;font-size:14px;box-sizing:border-box}.color-input-wrapper[data-v-d3dc5a63]{display:flex;align-items:center;gap:8px}.modal-color-picker[data-v-d3dc5a63]{width:40px;height:30px;padding:0;border:none;background:none;cursor:pointer}.modal-actions[data-v-d3dc5a63]{display:flex;justify-content:flex-end;gap:12px;margin-top:24px}.cancel-btn[data-v-d3dc5a63]{padding:6px 16px;background:#f5f5f5;border:1px solid #ddd;border-radius:4px;cursor:pointer;color:#666}.confirm-btn[data-v-d3dc5a63]{padding:6px 16px;background:#2196f3;border:none;border-radius:4px;cursor:pointer;color:#fff}.cancel-btn[data-v-d3dc5a63]:hover{background:#e0e0e0}.confirm-btn[data-v-d3dc5a63]: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-4cf50076]{width:100%;height:100vh;display:flex;flex-direction:column;background:#f5f5f5}.gallery-view[data-v-4cf50076]{flex:1;display:flex;flex-direction:column;overflow:hidden;padding:20px;position:relative}.gallery-header[data-v-4cf50076]{margin-bottom:20px;display:flex;justify-content:space-between;align-items:center;flex-shrink:0}.gallery-header h3[data-v-4cf50076]{margin:0;font-size:20px;color:#333}.label-summary[data-v-4cf50076]{display:flex;gap:8px}.label-badge[data-v-4cf50076]{padding:4px 10px;border-radius:12px;color:#fff;font-size:12px;font-weight:700}.gallery-grid[data-v-4cf50076]{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-4cf50076]{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-4cf50076]:hover{transform:translateY(-4px);box-shadow:0 4px 12px #00000026}.thumbnail-wrapper[data-v-4cf50076]{flex:1;overflow:hidden;position:relative}.img-meta[data-v-4cf50076]{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-4cf50076]{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-4cf50076]{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-4cf50076]{background:#2196f3;color:#fff}.action-btn.primary[data-v-4cf50076]:hover{background:#1976d2}.action-btn.success[data-v-4cf50076]{background:#4caf50;color:#fff}.action-btn.success[data-v-4cf50076]:hover{background:#388e3c}.editor-view[data-v-4cf50076]{flex:1;display:flex;flex-direction:column;height:100%}.editor-header[data-v-4cf50076]{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-4cf50076]{display:flex;align-items:center;gap:16px}.back-btn[data-v-4cf50076]{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-4cf50076]:hover{background:#f5f5f5;color:#333}.editor-content[data-v-4cf50076]{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}