@zzalai/leafer-point-annotation 1.1.1 → 1.1.2

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/src/App.vue CHANGED
@@ -7,10 +7,12 @@
7
7
  ref="pointAnnotation"
8
8
  :imageSource="imageSource"
9
9
  :options="editorOptions"
10
+ v-model:currentLayer="activeLayer"
10
11
  @pointChange="handlePointChange"
11
12
  @loadStart="handleLoadStart"
12
13
  @loadSuccess="handleLoadSuccess"
13
14
  @loadError="handleLoadError"
15
+ @update:currentLayer="handleLayerChange"
14
16
  />
15
17
  </div>
16
18
 
@@ -22,9 +24,28 @@
22
24
  type="text"
23
25
  id="imageUrl"
24
26
  v-model="imageUrl"
25
- placeholder="Enter image URL"
27
+ placeholder="Enter image URL (or leave empty for local upload)"
26
28
  />
27
29
  </div>
30
+
31
+ <div class="control-group">
32
+ <h3>Multi-Layer Mode</h3>
33
+ <div class="multi-layer-row">
34
+ <label>
35
+ <input type="checkbox" v-model="useMultiLayer" />
36
+ Enable multi-layer brush
37
+ </label>
38
+ <span class="layer-info">Active layer: <b>{{ activeLayer }}</b></span>
39
+ </div>
40
+ <div class="multi-layer-row" v-if="useMultiLayer">
41
+ <label>Switch layer (parent-driven):</label>
42
+ <select v-model="activeLayer">
43
+ <option v-for="l in layerConfig" :key="l.value" :value="l.value">{{ l.label }} ({{ l.value }})</option>
44
+ </select>
45
+ <button @click="printAllLayers">Log All Layers</button>
46
+ <button @click="printCurrentLayer">Log Current Layer</button>
47
+ </div>
48
+ </div>
28
49
 
29
50
  <div class="control-group">
30
51
  <button @click="fetchPointData">Get Point Data</button>
@@ -45,6 +66,46 @@
45
66
  <button @click="triggerFileInput">Import Canvas JSON</button>
46
67
  </div>
47
68
 
69
+ <div class="control-group">
70
+ <h3>Custom Toolbar (via ref)</h3>
71
+ <p class="subtle">隐藏组件自带工具栏后,父组件自定义按钮调用 API 替代</p>
72
+ <div class="multi-layer-row">
73
+ <label><input type="checkbox" v-model="showToolbar" /> 显示组件工具栏</label>
74
+ <label><input type="checkbox" v-model="showZoomController" /> 显示缩放控制器</label>
75
+ <label><input type="checkbox" v-model="enableBrush" /> 启用笔刷功能</label>
76
+ </div>
77
+ <div class="multi-layer-row">
78
+ <label>背景色: <input type="color" v-model="canvasBackground" style="width: 40px; height: 28px; vertical-align: middle; margin-left: 6px;" /></label>
79
+ <label>最小缩放: <input type="number" v-model.number="zoomMin" step="0.1" min="0.01" max="1" style="width: 70px; margin-left: 6px;" /></label>
80
+ <label>最大缩放: <input type="number" v-model.number="zoomMax" step="0.5" min="1" max="32" style="width: 70px; margin-left: 6px;" /></label>
81
+ </div>
82
+ <p class="subtle">注:画布背景色和缩放范围在图片加载时生效,修改后请重新加载图片</p>
83
+ <div class="multi-layer-row">
84
+ <button
85
+ :class="{ 'active-btn': currentToolValue === 'select' }"
86
+ @click="pointAnnotation?.selectTool()"
87
+ >选择工具</button>
88
+ <button
89
+ :class="{ 'active-btn': currentToolValue === 'point' }"
90
+ @click="pointAnnotation?.pointTool()"
91
+ >点标注工具</button>
92
+ <button
93
+ :class="{ 'active-btn': currentToolValue === 'brush' }"
94
+ @click="pointAnnotation?.brushTool(false)"
95
+ >笔刷工具</button>
96
+ <button
97
+ :class="{ 'active-btn': currentToolValue === 'eraser' }"
98
+ @click="pointAnnotation?.eraserTool()"
99
+ >橡皮擦</button>
100
+ </div>
101
+ <div class="multi-layer-row">
102
+ <button @click="pointAnnotation?.undo()">↶ 撤销</button>
103
+ <button @click="pointAnnotation?.redo()">↷ 重做</button>
104
+ <button @click="pointAnnotation?.deleteSelected()">🗑 删除当前</button>
105
+ <button @click="pointAnnotation?.clearAllAnnotationsAndBrush()">⚠ 清除全部</button>
106
+ </div>
107
+ </div>
108
+
48
109
  <div class="control-group">
49
110
  <h3>Brush Mask Export</h3>
50
111
  <div class="mask-options">
@@ -63,8 +124,95 @@
63
124
  </select>
64
125
  </label>
65
126
  </div>
66
- <button @click="exportMaskImage">Export Mask Image</button>
67
- <button @click="clearBrush">Clear Brush</button>
127
+ <button @click="exportMaskImage">Export Current Layer Mask</button>
128
+ <button @click="clearBrush">Clear Current Layer Brush</button>
129
+ <button @click="clearAllBrushLayers" v-if="useMultiLayer">Clear ALL Layers</button>
130
+ </div>
131
+
132
+ <div class="control-group">
133
+ <h3>Brush Style (via ref API)</h3>
134
+ <p class="subtle">通过组件 ref 调用 updateBrushStyle / getBrushStyle,可单独修改任一字段</p>
135
+ <div class="multi-layer-row">
136
+ <button @click="pointAnnotation?.updateBrushStyle({ color: '#ff4d4f' })">🟥 红色</button>
137
+ <button @click="pointAnnotation?.updateBrushStyle({ color: '#52c41a' })">🟩 绿色</button>
138
+ <button @click="pointAnnotation?.updateBrushStyle({ color: '#1890ff' })">🟦 蓝色</button>
139
+ <button @click="pointAnnotation?.updateBrushStyle({ color: '#faad14' })">🟨 黄色</button>
140
+ </div>
141
+ <div class="multi-layer-row">
142
+ <label>Size: {{ brushStyleSize }}</label>
143
+ <input type="range" min="5" max="300" :value="brushStyleSize" @input="onSizeChange($event)" />
144
+ <button @click="pointAnnotation?.updateBrushStyle({ size: 100 })">Reset Size</button>
145
+ </div>
146
+ <div class="multi-layer-row">
147
+ <label>Opacity: {{ brushStyleOpacity }}</label>
148
+ <input type="range" min="0.1" max="1" step="0.05" :value="brushStyleOpacity" @input="onOpacityChange($event)" />
149
+ <button @click="pointAnnotation?.updateBrushStyle({ opacity: 0.55 })">Reset Opacity</button>
150
+ </div>
151
+ <div class="multi-layer-row">
152
+ <label>Continuity: {{ brushStyleContinuity }}</label>
153
+ <input type="range" min="5" max="50" :value="brushStyleContinuity" @input="onContinuityChange($event)" />
154
+ <button @click="pointAnnotation?.updateBrushStyle({ continuity: 20 })">Reset Continuity</button>
155
+ </div>
156
+ <div class="multi-layer-row">
157
+ <button @click="printBrushStyle">Log Current Brush Style</button>
158
+ <button @click="pointAnnotation?.updateBrushStyle({ color: '#ff4d4f', size: 100, opacity: 0.55, continuity: 20 })">Restore All Defaults</button>
159
+ </div>
160
+ </div>
161
+
162
+ <div class="control-group">
163
+ <h3>Upload Mask as File to Backend API (via ref API)</h3>
164
+ <p class="subtle">直接拿 File 对象传给后端 form-data 上传接口,无需自己做 dataURL 转换</p>
165
+ <div class="multi-layer-row">
166
+ <label>上传接口 URL:</label>
167
+ <input type="text" v-model="uploadApiUrl" placeholder="https://your-api.com/upload" style="width: 280px;" />
168
+ </div>
169
+ <div class="multi-layer-row">
170
+ <label>上传字段名:</label>
171
+ <input type="text" v-model="uploadFieldName" placeholder="file" style="width: 120px;" />
172
+ <button @click="uploadMaskFile">📤 上传当前图层 mask 到后端</button>
173
+ <button @click="uploadAllMaskFiles">📦 上传所有图层 mask</button>
174
+ </div>
175
+ <div v-if="uploadLog" class="upload-log">{{ uploadLog }}</div>
176
+ </div>
177
+
178
+ <div class="control-group">
179
+ <h3>Point Label Editor (via ref API)</h3>
180
+ <p class="subtle">点标注后,父组件通过 updatePointAnnotationLabel(id, label) 修改标注点名称</p>
181
+ <div v-if="pointsData.length === 0" class="subtle">暂无点标注,请在画布上点击添加</div>
182
+ <div v-else class="point-list">
183
+ <div v-for="point in pointsData" :key="point.id" class="point-list-row">
184
+ <span class="point-num">#{{ point.sequenceNumber }}</span>
185
+ <input
186
+ type="text"
187
+ class="label-input"
188
+ :value="point.label || ''"
189
+ placeholder="输入自定义名称"
190
+ @input="(e: any) => updatePointLabel(point.id, (e.target as HTMLInputElement).value)"
191
+ />
192
+ <span class="point-id">{{ point.id }}</span>
193
+ </div>
194
+ </div>
195
+ </div>
196
+
197
+ <div class="control-group">
198
+ <h3>Generate Brush From Points (via ref API)</h3>
199
+ <p class="subtle">根据标注点轨迹生成闭合多边形填充区域(需 ≥3 个点),使用当前笔刷颜色和透明度</p>
200
+ <div class="multi-layer-row">
201
+ <button @click="generateBrushFromPoints">🎨 一键生成笔刷区域</button>
202
+ <button @click="pointAnnotation?.clearBrush()">🧹 清除当前笔刷层</button>
203
+ </div>
204
+ </div>
205
+
206
+ <div class="control-group" v-if="useMultiLayer">
207
+ <h3>Per-Layer Mask Export</h3>
208
+ <div class="multi-layer-row">
209
+ <label>Layer:</label>
210
+ <select v-model="exportLayerValue">
211
+ <option v-for="l in layerConfig" :key="l.value" :value="l.value">{{ l.label }} ({{ l.value }})</option>
212
+ </select>
213
+ <button @click="exportMaskByLayer">Export This Layer Mask</button>
214
+ </div>
215
+ <button @click="exportAllLayerMasks">Export ALL Layer Masks (zip by browser)</button>
68
216
  </div>
69
217
 
70
218
  <div class="control-group">
@@ -107,13 +255,16 @@
107
255
  <div class="status">
108
256
  <h2>Status</h2>
109
257
  <p>Image Load Status: {{ loadStatus }}</p>
258
+ <p>Multi-Layer Mode: {{ useMultiLayer ? 'ON' : 'OFF' }}</p>
259
+ <p v-if="useMultiLayer">Active Layer (v-model:currentLayer): <b>{{ activeLayer }}</b></p>
110
260
  </div>
111
261
  </div>
112
262
  </template>
113
263
 
114
264
  <script setup lang="ts">
115
- import { ref, computed } from 'vue'
265
+ import { ref, computed, watch } from 'vue'
116
266
  import PointAnnotation from './components/PointAnnotation.vue'
267
+ import type { OptionsSource } from './components/PointAnnotation.vue'
117
268
 
118
269
  // 图片URL
119
270
  const imageUrl = ref('')
@@ -125,21 +276,68 @@ const imageSource = computed(() => {
125
276
  }
126
277
  })
127
278
 
128
- // 编辑器选项
129
- const editorOptions = ref({
279
+ // --- 多图层相关 ---
280
+ const useMultiLayer = ref(false)
281
+ const layerConfig = [
282
+ { label: '前景区域', value: 'foreground' },
283
+ { label: '背景区域', value: 'background' },
284
+ { label: '忽略区域', value: 'ignore' }
285
+ ]
286
+ const activeLayer = ref<string>('foreground')
287
+ const exportLayerValue = ref<string>('foreground')
288
+
289
+ // 当切换到多图层模式时,重置 activeLayer 到默认第一个
290
+ watch(useMultiLayer, (enabled) => {
291
+ if (enabled) {
292
+ activeLayer.value = layerConfig[0].value
293
+ exportLayerValue.value = layerConfig[0].value
294
+ }
295
+ })
296
+
297
+ // 编辑器选项(根据 useMultiLayer 动态注入 brushLayers)
298
+ const baseOptions: OptionsSource = {
130
299
  pointStyle: {
131
- fill: '#f00',
132
- stroke: '#fff',
133
- strokeWidth: 2,
134
- width: 16,
135
- height: 16,
136
- radius: 8
300
+ circleFill: '#00f',
301
+ circleStroke: '#fff',
302
+ selectedCircleFill: '#f00',
303
+ selectedCircleStroke: '#fff'
137
304
  },
138
- selectedPointStyle: {
139
- fill: '#00f',
140
- stroke: '#fff',
141
- strokeWidth: 2,
142
- radius: 10
305
+ brushStyle: {
306
+ color: '#ff4d4f',
307
+ opacity: 0.55,
308
+ size: 100
309
+ }
310
+ }
311
+
312
+ const showToolbar = ref(false)
313
+ const showZoomController = ref(true)
314
+ const canvasBackground = ref('#f6f6f6')
315
+ const zoomMin = ref(0.2)
316
+ const zoomMax = ref(4)
317
+ const enableBrush = ref(true)
318
+
319
+ const editorOptions = computed<OptionsSource>(() => {
320
+ if (useMultiLayer.value) {
321
+ return {
322
+ ...baseOptions,
323
+ brushLayers: layerConfig,
324
+ maxBrushLayers: 8,
325
+ showToolbar: showToolbar.value,
326
+ showZoomController: showZoomController.value,
327
+ canvasBackground: canvasBackground.value,
328
+ zoomMin: zoomMin.value,
329
+ zoomMax: zoomMax.value,
330
+ enableBrush: enableBrush.value
331
+ }
332
+ }
333
+ return {
334
+ ...baseOptions,
335
+ showToolbar: showToolbar.value,
336
+ showZoomController: showZoomController.value,
337
+ canvasBackground: canvasBackground.value,
338
+ zoomMin: zoomMin.value,
339
+ zoomMax: zoomMax.value,
340
+ enableBrush: enableBrush.value
143
341
  }
144
342
  })
145
343
 
@@ -147,15 +345,128 @@ const editorOptions = ref({
147
345
  const loadStatus = ref('idle')
148
346
  const pointData = ref('')
149
347
  const pointAnnotation = ref<InstanceType<typeof PointAnnotation> | null>(null)
348
+ // 点标注列表(用于父组件修改 label 的 demo)
349
+ const pointsData = ref<any[]>([])
150
350
  const maskFormat = ref<'png' | 'jpeg'>('png')
151
351
  const maskForeground = ref<'black' | 'white'>('black')
352
+ const uploadApiUrl = ref<string>('https://your-api.com/upload')
353
+ const uploadFieldName = ref<string>('file')
354
+ const uploadLog = ref<string>('')
152
355
  const currentTool = ref<'select' | 'point' | 'brush' | 'eraser'>('select')
153
356
  const currentToolDisplay = ref('select')
154
357
  const lastAddedPointId = ref<string | null>(null)
155
358
 
359
+ // 用于 UI 轮询显示当前笔刷样式(每 500ms 触发一次刷新)
360
+ const brushStyleTick = ref(0)
361
+ setInterval(() => { brushStyleTick.value++ }, 500)
362
+
363
+ const currentBrushStyle = computed(() => {
364
+ brushStyleTick.value
365
+ return pointAnnotation.value?.getBrushStyle?.() || {
366
+ color: '#ff4d4f', opacity: 0.55, size: 100, continuity: 20, minSize: 5, maxSize: 300
367
+ }
368
+ })
369
+ const brushStyleSize = computed(() => currentBrushStyle.value.size)
370
+ const brushStyleOpacity = computed(() => currentBrushStyle.value.opacity)
371
+ const brushStyleContinuity = computed(() => currentBrushStyle.value.continuity)
372
+
373
+ const currentToolValue = computed(() => {
374
+ brushStyleTick.value
375
+ return pointAnnotation.value?.getCurrentTool?.() || 'select'
376
+ })
377
+
378
+ const onSizeChange = (e: Event) => {
379
+ const val = Number((e.target as HTMLInputElement).value)
380
+ pointAnnotation.value?.updateBrushStyle({ size: val })
381
+ }
382
+ const onOpacityChange = (e: Event) => {
383
+ const val = Number((e.target as HTMLInputElement).value)
384
+ pointAnnotation.value?.updateBrushStyle({ opacity: val })
385
+ }
386
+ const onContinuityChange = (e: Event) => {
387
+ const val = Number((e.target as HTMLInputElement).value)
388
+ pointAnnotation.value?.updateBrushStyle({ continuity: val })
389
+ }
390
+ const printBrushStyle = () => {
391
+ const style = pointAnnotation.value?.getBrushStyle?.()
392
+ console.log('Current brush style:', style)
393
+ alert('Brush style logged to console:\n' + JSON.stringify(style, null, 2))
394
+ }
395
+
396
+ // 父组件感知图层切换
397
+ const handleLayerChange = (layer: string) => {
398
+ console.log('[App.vue] Layer changed via event:', layer)
399
+ }
400
+
156
401
  // 处理点变化
157
402
  const handlePointChange = (data: any) => {
158
403
  pointData.value = JSON.stringify(data, null, 2)
404
+ pointsData.value = Array.isArray(data) ? [...data] : []
405
+ }
406
+
407
+ // 父组件修改某个标注点的 label(demo)
408
+ const updatePointLabel = (id: string, newLabel: string) => {
409
+ pointAnnotation.value?.updatePointAnnotationLabel(id, newLabel)
410
+ }
411
+
412
+ // 一键根据标注点生成笔刷区域(demo)
413
+ const generateBrushFromPoints = () => {
414
+ const result = pointAnnotation.value?.createBrushFromPoints()
415
+ if (result === false) {
416
+ alert('请先在画布上添加至少 3 个标注点')
417
+ }
418
+ }
419
+
420
+ // 上传当前图层 mask 为 File 到后端接口(demo)
421
+ const uploadMaskFile = async () => {
422
+ const file = await pointAnnotation.value?.getMaskFile(undefined, 'mask.png')
423
+ if (!file) {
424
+ uploadLog.value = '❌ 当前图层没有可导出的 mask,先在画布上画几笔或标注'
425
+ return
426
+ }
427
+ if (!uploadApiUrl.value) {
428
+ uploadLog.value = '❌ 请先填写上传接口 URL'
429
+ return
430
+ }
431
+ try {
432
+ const formData = new FormData()
433
+ formData.append(uploadFieldName.value || 'file', file)
434
+ uploadLog.value = `⏳ 正在上传 ${file.name} (${(file.size / 1024).toFixed(1)} KB) 到 ${uploadApiUrl.value}...`
435
+ const res = await fetch(uploadApiUrl.value, { method: 'POST', body: formData })
436
+ const text = await res.text()
437
+ uploadLog.value = `✅ 上传完成,状态 ${res.status},响应: ${text.substring(0, 300)}`
438
+ } catch (e) {
439
+ uploadLog.value = `❌ 上传失败: ${e}`
440
+ }
441
+ }
442
+
443
+ // 上传所有图层 mask 到后端(demo)
444
+ const uploadAllMaskFiles = async () => {
445
+ const blobs = await pointAnnotation.value?.getAllMaskBlobs()
446
+ if (!blobs || Object.keys(blobs).length === 0) {
447
+ uploadLog.value = '❌ 没有可导出的 mask'
448
+ return
449
+ }
450
+ if (!uploadApiUrl.value) {
451
+ uploadLog.value = '❌ 请先填写上传接口 URL'
452
+ return
453
+ }
454
+ try {
455
+ const formData = new FormData()
456
+ let totalSize = 0
457
+ for (const [layerValue, blob] of Object.entries(blobs)) {
458
+ const name = `mask_${layerValue}.png`
459
+ const file = new File([blob], name, { type: 'image/png' })
460
+ formData.append(uploadFieldName.value || 'file', file)
461
+ totalSize += blob.size
462
+ }
463
+ uploadLog.value = `⏳ 正在上传 ${Object.keys(blobs).length} 个文件 (共 ${(totalSize / 1024).toFixed(1)} KB) 到 ${uploadApiUrl.value}...`
464
+ const res = await fetch(uploadApiUrl.value, { method: 'POST', body: formData })
465
+ const text = await res.text()
466
+ uploadLog.value = `✅ 上传完成,状态 ${res.status},响应: ${text.substring(0, 300)}`
467
+ } catch (e) {
468
+ uploadLog.value = `❌ 上传失败: ${e}`
469
+ }
159
470
  }
160
471
 
161
472
  // 处理图片加载开始
@@ -360,6 +671,68 @@ const testRemovePoint = () => {
360
671
  }
361
672
  }
362
673
  }
674
+
675
+ // --- 多图层 API 测试方法 ---
676
+ const printAllLayers = () => {
677
+ if (!pointAnnotation.value) return
678
+ const layers = pointAnnotation.value.getAllLayers?.()
679
+ console.log('All layers:', layers)
680
+ alert('Layers printed to console: ' + JSON.stringify(layers, null, 2))
681
+ }
682
+
683
+ const printCurrentLayer = () => {
684
+ if (!pointAnnotation.value) return
685
+ const layer = pointAnnotation.value.getCurrentLayer?.()
686
+ console.log('Current layer:', layer)
687
+ alert('Current layer: ' + layer)
688
+ }
689
+
690
+ const clearAllBrushLayers = () => {
691
+ if (!pointAnnotation.value) return
692
+ pointAnnotation.value.clearAllBrushLayers?.()
693
+ alert('All brush layers cleared!')
694
+ }
695
+
696
+ const exportMaskByLayer = async () => {
697
+ if (!pointAnnotation.value) return
698
+ const mask = await pointAnnotation.value.exportMaskImageByLayer?.(
699
+ exportLayerValue.value,
700
+ maskFormat.value,
701
+ maskForeground.value
702
+ )
703
+ if (mask) {
704
+ const ext = maskFormat.value === 'png' ? 'png' : 'jpg'
705
+ const a = document.createElement('a')
706
+ a.href = mask
707
+ a.download = `brush-mask-${exportLayerValue.value}.${ext}`
708
+ a.click()
709
+ } else {
710
+ alert(`No brush data on layer "${exportLayerValue.value}"`)
711
+ }
712
+ }
713
+
714
+ const exportAllLayerMasks = async () => {
715
+ if (!pointAnnotation.value) return
716
+ const masks = await pointAnnotation.value.exportAllMaskImages?.(
717
+ maskFormat.value,
718
+ maskForeground.value
719
+ )
720
+ if (!masks || Object.keys(masks).length === 0) {
721
+ alert('No brush data on any layer')
722
+ return
723
+ }
724
+ const ext = maskFormat.value === 'png' ? 'png' : 'jpg'
725
+ // 浏览器逐一下载每个图层的 mask
726
+ Object.entries(masks).forEach(([layerValue, maskData], idx) => {
727
+ setTimeout(() => {
728
+ const a = document.createElement('a')
729
+ a.href = maskData
730
+ a.download = `brush-mask-${layerValue}.${ext}`
731
+ a.click()
732
+ }, idx * 300) // 延迟避免浏览器拦截连续下载
733
+ })
734
+ alert(`Started downloading ${Object.keys(masks).length} mask images`)
735
+ }
363
736
  </script>
364
737
 
365
738
  <style scoped>
@@ -464,4 +837,116 @@ pre {
464
837
  background-color: #f5f5f5;
465
838
  border-radius: 8px;
466
839
  }
840
+
841
+ .multi-layer-row {
842
+ display: flex;
843
+ align-items: center;
844
+ gap: 12px;
845
+ margin-bottom: 8px;
846
+ flex-wrap: wrap;
847
+ }
848
+
849
+ .multi-layer-row > label {
850
+ margin: 0;
851
+ display: flex;
852
+ align-items: center;
853
+ gap: 6px;
854
+ font-weight: normal;
855
+ }
856
+
857
+ .multi-layer-row > select {
858
+ padding: 6px 10px;
859
+ border: 1px solid #ddd;
860
+ border-radius: 4px;
861
+ font-size: 14px;
862
+ }
863
+
864
+ .multi-layer-row > button {
865
+ margin-right: 0;
866
+ }
867
+
868
+ .layer-info {
869
+ font-size: 14px;
870
+ color: #555;
871
+ }
872
+
873
+ .layer-info b {
874
+ color: #007bff;
875
+ }
876
+
877
+ .subtle {
878
+ font-size: 13px;
879
+ color: #888;
880
+ margin: 0 0 8px 0;
881
+ }
882
+
883
+ .active-btn {
884
+ background-color: var(--leafer-point-color-primary, #409eff);
885
+ color: white;
886
+ border-color: var(--leafer-point-color-primary, #409eff);
887
+ }
888
+
889
+ .active-btn:hover {
890
+ background-color: var(--leafer-point-color-primary, #409eff);
891
+ opacity: 0.9;
892
+ }
893
+
894
+ /* 点标注列表 - label 编辑 */
895
+ .point-list {
896
+ display: flex;
897
+ flex-direction: column;
898
+ gap: 6px;
899
+ max-height: 280px;
900
+ overflow-y: auto;
901
+ }
902
+
903
+ .point-list-row {
904
+ display: flex;
905
+ align-items: center;
906
+ gap: 10px;
907
+ padding: 6px 8px;
908
+ background-color: #f9f9f9;
909
+ border: 1px solid #eee;
910
+ border-radius: 4px;
911
+ }
912
+
913
+ .point-num {
914
+ min-width: 32px;
915
+ font-weight: bold;
916
+ color: #007bff;
917
+ font-size: 14px;
918
+ }
919
+
920
+ .label-input {
921
+ flex: 1;
922
+ padding: 4px 8px;
923
+ border: 1px solid #ddd;
924
+ border-radius: 4px;
925
+ font-size: 14px;
926
+ }
927
+
928
+ .label-input:focus {
929
+ outline: none;
930
+ border-color: #007bff;
931
+ }
932
+
933
+ .point-id {
934
+ font-size: 12px;
935
+ color: #aaa;
936
+ min-width: 80px;
937
+ }
938
+
939
+ .upload-log {
940
+ margin-top: 10px;
941
+ padding: 10px 12px;
942
+ background-color: #f9f9f9;
943
+ border: 1px solid #eee;
944
+ border-radius: 4px;
945
+ font-size: 13px;
946
+ color: #333;
947
+ word-break: break-all;
948
+ white-space: pre-wrap;
949
+ max-height: 200px;
950
+ overflow-y: auto;
951
+ }
467
952
  </style>