vue2-client 1.22.21 → 1.22.22

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.
Files changed (84) hide show
  1. package/.env.his +19 -19
  2. package/.idea/deployment.xml +14 -0
  3. package/.idea/inspectionProfiles/Project_Default.xml +11 -0
  4. package/.idea/misc.xml +5 -5
  5. package/package.json +1 -1
  6. package/src/base-client/components/common/HIS/HForm/HForm.vue +1186 -1186
  7. package/src/base-client/components/common/HIS/HTab/HTab.vue +790 -790
  8. package/src/base-client/components/common/XInspectionDetailDrawer/components/AiDeviceStatus.vue +25 -4
  9. package/src/base-client/components/common/XInspectionDetailDrawer/components/ImageAnnotationShow.vue +1371 -0
  10. package/src/base-client/components/common/XMarkdownViewer/demo.vue +102 -102
  11. package/src/base-client/components/his/XTransfer/index.md +327 -327
  12. package/src/utils/module-loader/VueSfcModuleCache.js +99 -99
  13. package/src/utils/module-loader/index.js +5 -5
  14. package/.history/.eslintrc_20260521171150.js +0 -74
  15. package/.history/.eslintrc_20260521171213.js +0 -74
  16. package/.history/src/base-client/components/common/HIS/HAddNativeForm/HAddNativeForm_20260601154443.vue +0 -726
  17. package/.history/src/base-client/components/common/HIS/HAddNativeForm/HAddNativeForm_20260601154700.vue +0 -478
  18. package/.history/src/base-client/components/common/HIS/HButtons/HButtons_20260512175435.vue +0 -706
  19. package/.history/src/base-client/components/common/HIS/HButtons/HButtons_20260512175450.vue +0 -694
  20. package/.history/src/base-client/components/common/HIS/HButtons/HButtons_20260611152602.vue +0 -755
  21. package/.history/src/base-client/components/common/HIS/HForm/HForm_20260513145941.vue +0 -524
  22. package/.history/src/base-client/components/common/HIS/HForm/HForm_20260513153133.vue +0 -731
  23. package/.history/src/base-client/components/common/HIS/HForm/HForm_20260513160316.vue +0 -525
  24. package/.history/src/base-client/components/common/HIS/HForm/HForm_20260601144150.vue +0 -1046
  25. package/.history/src/base-client/components/common/HIS/HFormTable/HFormTable_20260310142713.vue +0 -512
  26. package/.history/src/base-client/components/common/HIS/HFormTable/HFormTable_20260310145118.vue +0 -511
  27. package/.history/src/base-client/components/common/HIS/HFormTable/HFormTable_20260311094834.vue +0 -696
  28. package/.history/src/base-client/components/common/HIS/HFormTable/HFormTable_20260320143028.vue +0 -693
  29. package/.history/src/base-client/components/common/HIS/HFormTable/HFormTable_20260409101450.vue +0 -677
  30. package/.history/src/base-client/components/common/HIS/HTab/HTab_20260625173703.vue +0 -767
  31. package/.history/src/base-client/components/common/HIS/HTab/HTab_20260626112425.vue +0 -803
  32. package/.history/src/base-client/components/common/XAddNativeForm/XAddNativeForm_20260513140637.vue +0 -1455
  33. package/.history/src/base-client/components/common/XAddNativeForm/XAddNativeForm_20260513140935.vue +0 -1441
  34. package/.history/src/base-client/components/common/XAddNativeForm/XAddNativeForm_20260513150818.vue +0 -1441
  35. package/.history/src/base-client/components/common/XAddNativeForm/XAddNativeForm_20260513153119.vue +0 -1442
  36. package/.history/src/base-client/components/common/XAddNativeForm/XAddNativeForm_20260513153126.vue +0 -1486
  37. package/.history/src/base-client/components/common/XForm/XFormItem_20260513140854.vue +0 -1607
  38. package/.history/src/base-client/components/common/XMarkdownViewer/XMarkdownViewer_20260519140403.vue +0 -643
  39. package/.history/src/base-client/components/common/XMarkdownViewer/XMarkdownViewer_20260519140829.vue +0 -628
  40. package/.history/src/base-client/components/common/XMarkdownViewer/demo_20260519142824.vue +0 -104
  41. package/.history/src/base-client/components/common/XMarkdownViewer/demo_20260519143155.vue +0 -102
  42. package/.history/src/base-client/components/common/XReportGrid/XReport_20260309171231.vue +0 -1241
  43. package/.history/src/base-client/components/common/XReportGrid/XReport_20260309171441.vue +0 -1223
  44. package/.history/src/base-client/components/his/HAi/HAi_20260612174826.vue +0 -472
  45. package/.history/src/base-client/components/his/HAi/HAi_20260612175839.vue +0 -538
  46. package/.history/src/base-client/components/his/HAi/HAi_20260615103331.vue +0 -650
  47. package/.history/src/base-client/components/his/HAi/HAi_20260618140214.vue +0 -1396
  48. package/.history/src/base-client/components/his/HAi/HAi_20260618140556.vue +0 -1396
  49. package/.history/src/base-client/components/his/XHDescriptions/XHDescriptions_20260424134504.vue +0 -1469
  50. package/.history/src/base-client/components/his/XSidebar/XSidebar_20260610171133.vue +0 -788
  51. package/.history/src/base-client/components/his/XSidebar/XSidebar_20260610171151.vue +0 -780
  52. package/.history/src/base-client/components/his/XTransfer/XTransfer_20260511170841.vue +0 -585
  53. package/.history/src/base-client/components/his/XTransfer/XTransfer_20260511171138.vue +0 -787
  54. package/.history/src/base-client/components/his/XTransfer/XTransfer_20260512141830.vue +0 -739
  55. package/.history/src/components/STable/index_20260409155138.js +0 -806
  56. package/.history/src/components/STable/index_20260409155218.js +0 -814
  57. package/.history/src/expression/core/Expression_20260305164427.js +0 -1371
  58. package/.history/src/expression/core/Expression_20260305170258.js +0 -1358
  59. package/.history/src/expression/core/Program_20260305111830.js +0 -944
  60. package/.history/src/expression/core/Program_20260305112041.js +0 -931
  61. package/.history/src/logic/LogicRunner_20260304154306.js +0 -170
  62. package/.history/src/logic/LogicRunner_20260304155553.js +0 -112
  63. package/.history/src/logic/LogicRunner_20260305105834.js +0 -112
  64. package/.history/src/logic/LogicRunner_20260305112718.js +0 -129
  65. package/.history/src/logic/LogicRunner_20260305182436.js +0 -133
  66. package/.history/src/logic/LogicRunner_20260306151301.js +0 -213
  67. package/.history/src/logic/LogicRunner_20260306152419.js +0 -213
  68. package/.history/src/logic/plugins/common/DateTools_20260305154159.js +0 -61
  69. package/.history/src/logic/plugins/common/DateTools_20260305154217.js +0 -44
  70. package/.history/src/logic/plugins/common/DateTools_20260305161014.js +0 -44
  71. package/.history/src/logic/plugins/common/HttpTools_20260305164352.js +0 -80
  72. package/.history/src/logic/plugins/common/HttpTools_20260305170258.js +0 -75
  73. package/.history/src/logic/plugins/common/HttpTools_20260305171634.js +0 -75
  74. package/.history/src/logic/plugins/common/HttpTools_20260306152419.js +0 -72
  75. package/.history/src/services/api/restTools_20260427142149.js +0 -245
  76. package/.history/src/services/api/restTools_20260427142853.js +0 -230
  77. package/.history/src/services/api/restTools_20260519135558.js +0 -230
  78. package/.history/src/services/api/restTools_20260519140825.js +0 -230
  79. package/.history/src/services/api/restTools_20260519151223.js +0 -230
  80. package/.history/src/utils/indexedDB_20260306150918.js +0 -593
  81. package/.history/src/utils/indexedDB_20260306151301.js +0 -586
  82. package/.idea/codeStyles/Project.xml +0 -62
  83. package/.idea/codeStyles/codeStyleConfig.xml +0 -5
  84. package/preview-input-box.html +0 -180
@@ -0,0 +1,1371 @@
1
+ <template>
2
+ <div class="image-annotation-show" ref="wrapperRef">
3
+ <template v-show="!fullScreenMode">
4
+ <!-- 画布区域 -->
5
+ <div
6
+ class="canvas-container"
7
+ ref="imageWrapper"
8
+ @wheel="handleWheelZoom"
9
+ @mousedown="handleMouseDown"
10
+ @mousemove="handleMouseMove"
11
+ @mouseup="handleMouseUp"
12
+ @mouseleave="handleMouseUp"
13
+ >
14
+ <div
15
+ class="canvas-zoom-wrapper"
16
+ :style="{ width: canvasWrapperWidth + 'px', height: canvasWrapperHeight + 'px' }"
17
+ >
18
+ <div
19
+ class="canvas-inner"
20
+ :style="{ transform: `translate(${panOffsetX}px, ${panOffsetY}px) scale(${zoomLevel})` }"
21
+ :class="{ 'is-panning': isPanning }"
22
+ >
23
+ <img
24
+ ref="annotationImage"
25
+ :src="imageUrl"
26
+ @load="handleImageLoad"
27
+ @error="handleImageError"
28
+ class="annotation-image"
29
+ :class="{ 'image-loading': !imageLoaded }"
30
+ draggable="false"
31
+ :alt="imageAlt"
32
+ />
33
+ <canvas
34
+ ref="annotationCanvas"
35
+ class="annotation-canvas-layer"
36
+ :class="{ 'canvas-hidden': !imageLoaded }"
37
+ @click="handleCanvasClick"
38
+ ></canvas>
39
+ </div>
40
+ </div>
41
+ </div>
42
+ <!-- 全屏按钮 -->
43
+ <a-tooltip title="全屏预览">
44
+ <a-button
45
+ type="primary"
46
+ icon="fullscreen"
47
+ class="fullscreen-btn"
48
+ @click="openFullscreen"
49
+ />
50
+ </a-tooltip>
51
+
52
+ <!-- 空状态 -->
53
+ <div v-if="showEmpty && !imageLoaded && !loading" class="canvas-empty">
54
+ <slot name="empty">
55
+ <span>暂无图片</span>
56
+ </slot>
57
+ </div>
58
+
59
+ <!-- 加载中 -->
60
+ <div v-if="loading" class="canvas-loading">
61
+ <a-spin size="large" />
62
+ </div>
63
+ </template>
64
+ <!-- 全屏预览 -->
65
+ <div v-if="isFullscreen" class="fullscreen-preview-wrapper" @wheel.prevent.stop>
66
+ <!-- 图片预览区域 -->
67
+ <div
68
+ class="image-preview-container"
69
+ ref="fullscreenContainer"
70
+ @wheel="handleFullscreenWheel"
71
+ @mousedown="startFullscreenDrag"
72
+ :class="{ dragging: isFullscreenDragging }"
73
+ tabindex="0"
74
+ @keydown="handleFullscreenKeydown"
75
+ >
76
+ <div class="image-wrapper" :style="fullscreenWrapperStyle">
77
+ <div class="image-inner" ref="fullscreenImageWrapper" :style="{ transform: `scale(${fullscreenScale})`, transformOrigin: 'center center' }">
78
+ <div class="preview-image-wrapper">
79
+ <img
80
+ :src="imageUrl"
81
+ alt="全屏预览图片"
82
+ class="preview-image"
83
+ ref="fullscreenImage"
84
+ @load="handleFullscreenImageLoad"
85
+ />
86
+ <canvas
87
+ ref="fullscreenCanvas"
88
+ class="fullscreen-canvas"
89
+ :class="{ 'canvas-hidden': !showAnnotations }"
90
+ ></canvas>
91
+ </div>
92
+ </div>
93
+ </div>
94
+ </div>
95
+
96
+ <!-- 底部工具栏 -->
97
+ <div class="preview-toolbar bottom-toolbar" @wheel.prevent.stop>
98
+ <div class="toolbar-content">
99
+ <a-tooltip :title="showAnnotations ? '隐藏标注' : '显示标注'">
100
+ <a-button
101
+ type="primary"
102
+ :icon="showAnnotations ? 'eye-invisible' : 'eye'"
103
+ @click="toggleAnnotations"
104
+ size="small"
105
+ class="toolbar-btn"
106
+ />
107
+ </a-tooltip>
108
+ <div class="toolbar-divider"></div>
109
+ <a-tooltip title="关闭">
110
+ <a-button
111
+ type="primary"
112
+ icon="close"
113
+ @click="closeFullscreen"
114
+ size="small"
115
+ class="toolbar-btn close-btn"
116
+ />
117
+ </a-tooltip>
118
+ </div>
119
+ </div>
120
+ </div>
121
+ </div>
122
+ </template>
123
+
124
+ <script>
125
+ export default {
126
+ name: 'ImageAnnotationShow',
127
+ props: {
128
+ imageUrl: {
129
+ type: String,
130
+ default: ''
131
+ },
132
+ imageAlt: {
133
+ type: String,
134
+ default: '标注图片'
135
+ },
136
+ selectedIndex: {
137
+ type: Number,
138
+ default: -1
139
+ },
140
+ annotations: {
141
+ type: Array,
142
+ default: () => []
143
+ },
144
+ showEmpty: {
145
+ type: Boolean,
146
+ default: true
147
+ },
148
+ loading: {
149
+ type: Boolean,
150
+ default: false
151
+ },
152
+ // 全屏模式
153
+ fullScreenMode: {
154
+ type: Boolean,
155
+ default: false
156
+ }
157
+ },
158
+ data() {
159
+ return {
160
+ imageLoaded: false,
161
+ imageInfo: {},
162
+ canvas: null,
163
+ ctx: null,
164
+ zoomLevel: 1,
165
+ canvasWrapperWidth: 0,
166
+ canvasWrapperHeight: 0,
167
+ localSelectedIndex: -1,
168
+ panOffsetX: 0,
169
+ panOffsetY: 0,
170
+ isPanning: false,
171
+ panStartX: 0,
172
+ panStartY: 0,
173
+ panOriginX: 0,
174
+ panOriginY: 0,
175
+ hasJustPanned: false,
176
+ // 全屏相关
177
+ isFullscreen: false,
178
+ showAnnotations: true,
179
+ fullscreenScale: 1,
180
+ fullscreenTranslateX: 0,
181
+ fullscreenTranslateY: 0,
182
+ isFullscreenDragging: false,
183
+ fullscreenDragStartX: 0,
184
+ fullscreenDragStartY: 0
185
+ }
186
+ },
187
+ computed: {
188
+ fullscreenWrapperStyle() {
189
+ return {
190
+ transform: `translate(${this.fullscreenTranslateX}px, ${this.fullscreenTranslateY}px)`,
191
+ cursor: this.isFullscreenDragging ? 'grabbing' : 'grab'
192
+ }
193
+ }
194
+ },
195
+ watch: {
196
+ annotations: {
197
+ handler() {
198
+ this.$nextTick(() => this.draw())
199
+ },
200
+ deep: true
201
+ },
202
+ selectedIndex(val) {
203
+ this.localSelectedIndex = val
204
+ },
205
+ localSelectedIndex() {
206
+ this.draw()
207
+ },
208
+ imageUrl() {
209
+ this.imageLoaded = false
210
+ }
211
+ },
212
+ mounted() {
213
+ window.addEventListener('resize', this.handleWindowResize)
214
+ document.addEventListener('keydown', this.handleGlobalKeydown)
215
+ },
216
+ beforeDestroy() {
217
+ window.removeEventListener('resize', this.handleWindowResize)
218
+ document.removeEventListener('keydown', this.handleGlobalKeydown)
219
+ document.removeEventListener('mousemove', this.onFullscreenDrag)
220
+ document.removeEventListener('mouseup', this.endFullscreenDrag)
221
+ },
222
+ methods: {
223
+ handleImageLoad() {
224
+ this.imageLoaded = true
225
+ this.$nextTick(() => {
226
+ this.alignCanvasToImage()
227
+ const img = this.$refs.annotationImage
228
+ this.imageInfo = {
229
+ width: img.naturalWidth,
230
+ height: img.naturalHeight
231
+ }
232
+ this.fitToScreen()
233
+ this.draw()
234
+ })
235
+ },
236
+
237
+ handleImageError() {
238
+ this.imageLoaded = false
239
+ },
240
+
241
+ alignCanvasToImage() {
242
+ const img = this.$refs.annotationImage
243
+ const wrapper = this.$refs.imageWrapper
244
+ const canvas = this.$refs.annotationCanvas
245
+ if (!img || !wrapper || !canvas) return
246
+
247
+ const canvasContainer = canvas.parentElement
248
+ if (!canvasContainer) return
249
+
250
+ const w = img.naturalWidth
251
+ const h = img.naturalHeight
252
+
253
+ canvasContainer.style.width = w + 'px'
254
+ canvasContainer.style.height = h + 'px'
255
+ img.style.width = w + 'px'
256
+ img.style.height = h + 'px'
257
+
258
+ canvas.width = w
259
+ canvas.height = h
260
+ canvas.style.width = w + 'px'
261
+ canvas.style.height = h + 'px'
262
+ canvas.style.position = 'absolute'
263
+ canvas.style.left = '0'
264
+ canvas.style.top = '0'
265
+
266
+ this.canvas = canvas
267
+ this.ctx = canvas.getContext('2d')
268
+ this.updateZoomWrapperSize()
269
+ },
270
+
271
+ updateZoomWrapperSize() {
272
+ if (!this.canvas) return
273
+ this.canvasWrapperWidth = this.canvas.width * this.zoomLevel
274
+ this.canvasWrapperHeight = this.canvas.height * this.zoomLevel
275
+ },
276
+
277
+ handleWindowResize() {
278
+ if (!this.imageLoaded) return
279
+ this.alignCanvasToImage()
280
+ this.draw()
281
+ },
282
+
283
+ fitToScreen() {
284
+ if (!this.imageLoaded) return
285
+ const wrapper = this.$refs.imageWrapper
286
+ const img = this.$refs.annotationImage
287
+ if (!wrapper || !img) return
288
+
289
+ const viewWidth = wrapper.clientWidth
290
+ const viewHeight = wrapper.clientHeight
291
+ const imgWidth = img.naturalWidth || img.width
292
+ const imgHeight = img.naturalHeight || img.height
293
+
294
+ const scaleX = (viewWidth / imgWidth) - 0.01
295
+ const scaleY = (viewHeight / imgHeight) - 0.01
296
+ this.zoomLevel = Math.min(scaleX, scaleY, 1)
297
+ this.panOffsetX = 0
298
+ this.panOffsetY = 0
299
+ this.updateZoomWrapperSize()
300
+ },
301
+
302
+ handleWheelZoom(e) {
303
+ if (e.ctrlKey && this.imageLoaded) {
304
+ e.preventDefault()
305
+ this.zoomLevel = e.deltaY < 0
306
+ ? Math.min(3, this.zoomLevel + 0.1)
307
+ : Math.max(0.3, this.zoomLevel - 0.1)
308
+ this.updateZoomWrapperSize()
309
+ }
310
+ },
311
+
312
+ handleMouseDown(e) {
313
+ if (e.button !== 0) return
314
+ this.isPanning = true
315
+ this.panStartX = e.clientX
316
+ this.panStartY = e.clientY
317
+ this.panOriginX = this.panOffsetX
318
+ this.panOriginY = this.panOffsetY
319
+ this.hasJustPanned = false
320
+ },
321
+
322
+ handleMouseMove(e) {
323
+ if (!this.isPanning) return
324
+ const dx = e.clientX - this.panStartX
325
+ const dy = e.clientY - this.panStartY
326
+ if (Math.abs(dx) > 2 || Math.abs(dy) > 2) {
327
+ this.hasJustPanned = true
328
+ }
329
+ this.panOffsetX = this.panOriginX + dx
330
+ this.panOffsetY = this.panOriginY + dy
331
+ },
332
+
333
+ handleMouseUp() {
334
+ this.isPanning = false
335
+ },
336
+
337
+ handleCanvasClick(e) {
338
+ if (this.hasJustPanned) return
339
+
340
+ const pos = this.getMousePosition(e)
341
+ let hitIndex = -1
342
+
343
+ for (let i = this.annotations.length - 1; i >= 0; i--) {
344
+ const ann = this.annotations[i]
345
+ const type = this.getAnnotationType(ann)
346
+ let hit = false
347
+
348
+ if (type === 'rect') {
349
+ hit = this.isPointInRect(pos, ann.box)
350
+ } else if (type === 'polygon') {
351
+ hit = this.isPointInPolygon(pos, ann.mask)
352
+ } else if (type === 'line') {
353
+ const line = ann.line || { start: ann.points[0], end: ann.points[1] }
354
+ hit = this.isPointNearLine(pos, line.start, line.end)
355
+ } else if (type === 'point') {
356
+ for (const point of this.getPointAnnotations(ann)) {
357
+ if (this.isPointNearPoint(pos, point)) {
358
+ hit = true
359
+ break
360
+ }
361
+ }
362
+ } else if (type === 'hrnet') {
363
+ hit = this.isPointNearHrnetAnnotation(pos, ann)
364
+ }
365
+
366
+ if (hit) {
367
+ hitIndex = i
368
+ break
369
+ }
370
+ }
371
+
372
+ // 更新选中索引并同步触发重绘
373
+ this.localSelectedIndex = hitIndex
374
+ this.draw()
375
+ this.$emit('update:selectedIndex', hitIndex)
376
+ this.$emit('select-change', { index: hitIndex, annotation: hitIndex >= 0 ? this.annotations[hitIndex] : null })
377
+ },
378
+
379
+ getMousePosition(e) {
380
+ const canvasEl = this.$refs.annotationCanvas
381
+ if (!canvasEl) return { x: 0, y: 0 }
382
+ const rect = canvasEl.getBoundingClientRect()
383
+ const scaleX = canvasEl.width / rect.width
384
+ const scaleY = canvasEl.height / rect.height
385
+ return {
386
+ x: (e.clientX - rect.left) * scaleX,
387
+ y: (e.clientY - rect.top) * scaleY
388
+ }
389
+ },
390
+
391
+ isPointInRect(p, box) {
392
+ return p.x >= box.x && p.x <= box.x + box.width &&
393
+ p.y >= box.y && p.y <= box.y + box.height
394
+ },
395
+
396
+ isPointInPolygon(p, vertices) {
397
+ let inside = false
398
+ for (let i = 0, j = vertices.length - 1; i < vertices.length; j = i++) {
399
+ const xi = vertices[i].x; const yi = vertices[i].y
400
+ const xj = vertices[j].x; const yj = vertices[j].y
401
+ const intersect = ((yi > p.y) !== (yj > p.y)) &&
402
+ (p.x < (xj - xi) * (p.y - yi) / (yj - yi) + xi)
403
+ if (intersect) inside = !inside
404
+ }
405
+ return inside
406
+ },
407
+
408
+ isPointNearLine(p, start, end, threshold = 8) {
409
+ const A = p.x - start.x
410
+ const B = p.y - start.y
411
+ const C = end.x - start.x
412
+ const D = end.y - start.y
413
+ const dot = A * C + B * D
414
+ const lenSq = C * C + D * D
415
+ const param = lenSq !== 0 ? dot / lenSq : -1
416
+ let xx, yy
417
+ if (param < 0) { xx = start.x; yy = start.y }
418
+ else if (param > 1) { xx = end.x; yy = end.y }
419
+ else { xx = start.x + param * C; yy = start.y + param * D }
420
+ return Math.sqrt((p.x - xx) ** 2 + (p.y - yy) ** 2) <= threshold
421
+ },
422
+
423
+ isPointNearPoint(p, point, threshold = 10) {
424
+ return Math.sqrt((p.x - point.x) ** 2 + (p.y - point.y) ** 2) <= threshold
425
+ },
426
+
427
+ // ========== 绘制总入口 ==========
428
+ draw() {
429
+ if (!this.ctx || !this.canvas || !this.imageLoaded) return
430
+ this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
431
+
432
+ this.annotations.forEach((ann, index) => {
433
+ const type = this.getAnnotationType(ann)
434
+ const isSelected = index === this.localSelectedIndex
435
+ switch (type) {
436
+ case 'rect':
437
+ this.drawRect(ann, index, isSelected)
438
+ break
439
+ case 'polygon':
440
+ this.drawPolygon(ann, index, isSelected)
441
+ break
442
+ case 'line':
443
+ this.drawLine(ann, index, isSelected)
444
+ break
445
+ case 'point':
446
+ this.drawPoint(ann, index, isSelected)
447
+ break
448
+ case 'hrnet':
449
+ this.drawHrnet(ann, index, isSelected)
450
+ break
451
+ }
452
+ })
453
+ // 调用全屏方法
454
+ if (this.fullScreenMode) {
455
+ this.openFullscreen()
456
+ }
457
+ },
458
+
459
+ getAnnotationType(ann) {
460
+ if (this.isHrnetAnnotation(ann)) return 'hrnet'
461
+ if (ann.mask && Array.isArray(ann.mask) && ann.mask.length > 0) return 'polygon'
462
+ if (ann.points && Array.isArray(ann.points) && ann.points.length >= 2) return 'line'
463
+ if (ann.line) return 'line'
464
+ if (ann.type === 'point' || ann.point) return 'point'
465
+ if (ann.centerline_samples && Array.isArray(ann.centerline_samples) && ann.centerline_samples.length > 0) return 'point'
466
+ if (ann.box) return 'rect'
467
+ return 'rect'
468
+ },
469
+
470
+ isHrnetAnnotation(ann) {
471
+ if (!ann || typeof ann !== 'object') return false
472
+ if (Array.isArray(ann.centerline) && ann.centerline.length > 0) return true
473
+ if (Array.isArray(ann.keypoints) && ann.keypoints.length > 0) return true
474
+ return String(ann.model_type || ann.modelType || '').toLowerCase().includes('hrnet')
475
+ },
476
+
477
+ getPointXY(point) {
478
+ if (Array.isArray(point)) {
479
+ return {
480
+ x: Number(point[0]),
481
+ y: Number(point[1]),
482
+ score: Number(point[2] !== undefined && point[2] !== null ? point[2] : 0)
483
+ }
484
+ }
485
+ if (point && typeof point === 'object') {
486
+ const score = point.score !== undefined && point.score !== null
487
+ ? point.score
488
+ : point.s !== undefined && point.s !== null
489
+ ? point.s
490
+ : point.confidence !== undefined && point.confidence !== null
491
+ ? point.confidence
492
+ : point.value !== undefined && point.value !== null
493
+ ? point.value
494
+ : 0
495
+ return {
496
+ x: Number(point.x),
497
+ y: Number(point.y),
498
+ score: Number(score)
499
+ }
500
+ }
501
+ return { x: NaN, y: NaN, score: 0 }
502
+ },
503
+
504
+ getHrnetCenterlineGroups(ann) {
505
+ const groups = []
506
+
507
+ if (Array.isArray(ann.centerline)) {
508
+ ann.centerline.forEach(item => {
509
+ const points = item && Array.isArray(item.points) ? item.points : item
510
+ if (Array.isArray(points) && points.length > 0) {
511
+ groups.push(points.map(this.getPointXY).filter(p => Number.isFinite(p.x) && Number.isFinite(p.y)))
512
+ }
513
+ })
514
+ }
515
+
516
+ if (Array.isArray(ann.centerline_samples) && ann.centerline_samples.length > 0) {
517
+ groups.push(ann.centerline_samples.map(this.getPointXY).filter(p => Number.isFinite(p.x) && Number.isFinite(p.y)))
518
+ }
519
+
520
+ return groups.filter(points => points.length > 0)
521
+ },
522
+
523
+ getHrnetKeypoints(ann) {
524
+ return Array.isArray(ann.keypoints)
525
+ ? ann.keypoints.map(this.getPointXY).filter(p => Number.isFinite(p.x) && Number.isFinite(p.y))
526
+ : []
527
+ },
528
+
529
+ isPointNearHrnetAnnotation(pos, ann) {
530
+ const keypointHit = this.getHrnetKeypoints(ann).some(point => this.isPointNearPoint(pos, point, 14))
531
+ if (keypointHit) return true
532
+
533
+ return this.getHrnetCenterlineGroups(ann).some(points => {
534
+ return points.some(point => this.isPointNearPoint(pos, point, 10))
535
+ })
536
+ },
537
+
538
+ getPointAnnotations(ann) {
539
+ if (ann.point && typeof ann.point === 'object') {
540
+ return [{ x: Number(ann.point.x), y: Number(ann.point.y) }]
541
+ }
542
+ if (Array.isArray(ann.centerline_samples)) {
543
+ return ann.centerline_samples.map(p => {
544
+ if (Array.isArray(p)) {
545
+ return { x: Number(p[0]), y: Number(p[1]) }
546
+ }
547
+ return { x: Number(p.x), y: Number(p.y) }
548
+ }).filter(p => Number.isFinite(p.x) && Number.isFinite(p.y))
549
+ }
550
+ return []
551
+ },
552
+
553
+ // ========== 矩形绘制 ==========
554
+ drawRect(ann, index, isSelected) {
555
+ const box = ann.box
556
+ if (!box) return
557
+
558
+ if (isSelected) {
559
+ this.ctx.strokeStyle = '#fa8c16'
560
+ this.ctx.fillStyle = 'rgba(250, 140, 22, 0.15)'
561
+ this.ctx.lineWidth = 2.5
562
+ } else {
563
+ this.ctx.strokeStyle = '#1890ff'
564
+ this.ctx.fillStyle = 'rgba(24, 144, 255, 0.08)'
565
+ this.ctx.lineWidth = 2
566
+ }
567
+
568
+ this.ctx.fillRect(box.x, box.y, box.width, box.height)
569
+ this.ctx.strokeRect(box.x, box.y, box.width, box.height)
570
+
571
+ this.drawLabel(
572
+ box.x,
573
+ box.y,
574
+ `${index + 1}. ${ann.objectName || '未命名'}`,
575
+ ann.confidence,
576
+ isSelected
577
+ )
578
+ },
579
+
580
+ // ========== 多边形绘制 ==========
581
+ drawPolygon(ann, index, isSelected) {
582
+ const mask = ann.mask
583
+ if (!mask || mask.length < 3) return
584
+
585
+ if (isSelected) {
586
+ this.ctx.strokeStyle = '#fa8c16'
587
+ this.ctx.fillStyle = 'rgba(250, 140, 22, 0.15)'
588
+ this.ctx.lineWidth = 2.5
589
+ } else {
590
+ this.ctx.strokeStyle = '#1890ff'
591
+ this.ctx.fillStyle = 'rgba(24, 144, 255, 0.08)'
592
+ this.ctx.lineWidth = 2
593
+ }
594
+ this.ctx.lineCap = 'round'
595
+ this.ctx.lineJoin = 'round'
596
+
597
+ this.ctx.beginPath()
598
+ this.ctx.moveTo(mask[0].x, mask[0].y)
599
+ for (let i = 1; i < mask.length; i++) {
600
+ this.ctx.lineTo(mask[i].x, mask[i].y)
601
+ }
602
+ this.ctx.closePath()
603
+ this.ctx.fill()
604
+ this.ctx.stroke()
605
+
606
+ const minX = Math.min(...mask.map(p => p.x))
607
+ const minY = Math.min(...mask.map(p => p.y))
608
+ this.drawLabel(
609
+ minX,
610
+ minY,
611
+ `${index + 1}. ${ann.objectName || '未命名'}`,
612
+ ann.confidence,
613
+ isSelected
614
+ )
615
+ },
616
+
617
+ // ========== 线段绘制 ==========
618
+ drawLine(ann, index, isSelected) {
619
+ const line = ann.line || (ann.points && ann.points.length >= 2 ? { start: ann.points[0], end: ann.points[1] } : null)
620
+ if (!line || !line.start || !line.end) return
621
+
622
+ const color = isSelected ? '#fa8c16' : '#1890ff'
623
+ this.ctx.strokeStyle = color
624
+ this.ctx.fillStyle = color
625
+ this.ctx.lineWidth = isSelected ? 2.5 : 2
626
+ this.ctx.lineCap = 'round'
627
+
628
+ this.ctx.beginPath()
629
+ this.ctx.moveTo(line.start.x, line.start.y)
630
+ this.ctx.lineTo(line.end.x, line.end.y)
631
+ this.ctx.stroke()
632
+
633
+ // 端点
634
+ ;[line.start, line.end].forEach(p => {
635
+ this.ctx.beginPath()
636
+ this.ctx.arc(p.x, p.y, 4, 0, Math.PI * 2)
637
+ this.ctx.fillStyle = '#ffffff'
638
+ this.ctx.fill()
639
+ this.ctx.stroke()
640
+ })
641
+
642
+ const midX = (line.start.x + line.end.x) / 2
643
+ const midY = (line.start.y + line.end.y) / 2
644
+ this.drawLabel(
645
+ midX,
646
+ midY - 8,
647
+ `${index + 1}. ${ann.objectName || '线段'}`,
648
+ ann.confidence,
649
+ isSelected
650
+ )
651
+ },
652
+
653
+ // ========== 点绘制 ==========
654
+ drawPoint(ann, index, isSelected) {
655
+ const points = this.getPointAnnotations(ann)
656
+ if (points.length === 0) return
657
+
658
+ const radius = isSelected ? 11 : 10 // 热力图点一般稍大
659
+ const centerAlpha = isSelected ? 0.9 : 0.5 // 中心不透明度
660
+ const edgeAlpha = 0 // 边缘完全透明
661
+
662
+ points.forEach(p => {
663
+ const px = p.x
664
+ const py = p.y
665
+ if (px === undefined || py === undefined) return
666
+
667
+ // 创建径向渐变:内圈到外圈
668
+ const gradient = this.ctx.createRadialGradient(px, py, 0, px, py, radius)
669
+ gradient.addColorStop(0, `rgba(255, 60, 60, ${centerAlpha})`) // 中心红色
670
+ gradient.addColorStop(1, `rgba(255, 60, 60, ${edgeAlpha})`) // 边缘透明
671
+ this.ctx.beginPath()
672
+ this.ctx.arc(px, py, radius, 0, Math.PI * 2)
673
+ this.ctx.fillStyle = gradient
674
+ this.ctx.fill()
675
+ })
676
+
677
+ if (points.length > 0) {
678
+ const first = points[0]
679
+ const fx = first.x
680
+ const fy = first.y
681
+ if (fx !== undefined && fy !== undefined) {
682
+ this.drawLabel(
683
+ fx + 10,
684
+ fy - 10,
685
+ `${index + 1}. ${ann.objectName || '点'}`,
686
+ ann.confidence,
687
+ isSelected,
688
+ 'red'
689
+ )
690
+ }
691
+ }
692
+ },
693
+
694
+ // ========== HRNet 中心线/关键点绘制 ==========
695
+ drawHrnet(ann, index, isSelected) {
696
+ const centerlineGroups = this.getHrnetCenterlineGroups(ann)
697
+ const keypoints = this.getHrnetKeypoints(ann)
698
+
699
+ centerlineGroups.forEach(points => {
700
+ this.drawHrnetCenterline(points, isSelected)
701
+ })
702
+
703
+ keypoints.forEach((point, pointIndex) => {
704
+ this.drawHrnetKeypoint(point, pointIndex, isSelected)
705
+ })
706
+
707
+ const firstCenterlineGroup = centerlineGroups.find(points => points.length > 0)
708
+ const labelPoint = keypoints[0] || (firstCenterlineGroup ? firstCenterlineGroup[0] : null)
709
+ if (labelPoint) {
710
+ this.drawLabel(
711
+ labelPoint.x + 12,
712
+ labelPoint.y - 12,
713
+ `${index + 1}. ${ann.objectName || ann.class_name || 'HRNet'}`,
714
+ ann.confidence,
715
+ isSelected,
716
+ 'green'
717
+ )
718
+ }
719
+ },
720
+
721
+ drawHrnetCenterline(points, isSelected) {
722
+ const sampledPoints = points.slice(0, 1800)
723
+
724
+ if (points.length >= 2) {
725
+ this.ctx.beginPath()
726
+ this.ctx.moveTo(points[0].x, points[0].y)
727
+ for (let i = 1; i < points.length; i++) {
728
+ this.ctx.lineTo(points[i].x, points[i].y)
729
+ }
730
+ this.ctx.strokeStyle = isSelected ? 'rgba(250, 140, 22, 0.65)' : 'rgba(255, 190, 11, 0.32)'
731
+ this.ctx.lineWidth = isSelected ? 3 : 2
732
+ this.ctx.lineCap = 'round'
733
+ this.ctx.lineJoin = 'round'
734
+ this.ctx.stroke()
735
+ }
736
+
737
+ sampledPoints.forEach(point => {
738
+ const score = Math.max(0, Math.min(1, Number(point.score || 0)))
739
+ const radius = Math.max(1.8, 2.4 + score * 5.5) + (isSelected ? 1.2 : 0)
740
+
741
+ this.ctx.beginPath()
742
+ this.ctx.arc(point.x, point.y, radius, 0, Math.PI * 2)
743
+ this.ctx.fillStyle = score > 0.55
744
+ ? `rgba(255, 45, 26, ${Math.max(0.18, Math.min(0.58, 0.18 + score * 0.34))})`
745
+ : `rgba(255, 190, 11, ${Math.max(0.12, Math.min(0.46, 0.12 + score * 0.34))})`
746
+ this.ctx.fill()
747
+ })
748
+ },
749
+
750
+ drawHrnetKeypoint(point, index, isSelected) {
751
+ const score = Math.max(0, Math.min(1, Number(point.score || 0)))
752
+ const haloRadius = isSelected ? 15 : 13
753
+ const dotRadius = isSelected ? 7 : 6
754
+
755
+ this.ctx.beginPath()
756
+ this.ctx.arc(point.x, point.y, haloRadius, 0, Math.PI * 2)
757
+ this.ctx.fillStyle = 'rgba(0, 230, 118, 0.22)'
758
+ this.ctx.fill()
759
+
760
+ this.ctx.beginPath()
761
+ this.ctx.arc(point.x, point.y, dotRadius, 0, Math.PI * 2)
762
+ this.ctx.fillStyle = '#00e676'
763
+ this.ctx.fill()
764
+ this.ctx.strokeStyle = isSelected ? '#ffffff' : '#06351b'
765
+ this.ctx.lineWidth = 2
766
+ this.ctx.stroke()
767
+
768
+ const labelText = `#${index + 1} ${(score * 100).toFixed(1)}%`
769
+ this.ctx.font = 'bold 12px Arial'
770
+ this.ctx.lineWidth = 3
771
+ this.ctx.strokeStyle = '#06351b'
772
+ this.ctx.fillStyle = '#ffffff'
773
+ this.ctx.strokeText(labelText, point.x + 10, Math.max(14, point.y - 8))
774
+ this.ctx.fillText(labelText, point.x + 10, Math.max(14, point.y - 8))
775
+ },
776
+
777
+ // ========== 标签绘制 ==========
778
+ drawLabel(x, y, text, confidence, isSelected, accent) {
779
+ let labelText = text
780
+ if (confidence !== undefined && confidence !== null) {
781
+ labelText += ` (${(confidence * 100).toFixed(1)}%)`
782
+ }
783
+
784
+ const labelPadding = 6
785
+ const labelHeight = 22
786
+ this.ctx.font = 'bold 16px Arial'
787
+ const labelWidth = this.ctx.measureText(labelText).width + labelPadding * 2
788
+
789
+ const labelY = y >= labelHeight ? y - labelHeight : y
790
+ const textY = y >= labelHeight ? y - 7 : y + 15
791
+
792
+ if (accent === 'red') {
793
+ this.ctx.fillStyle = isSelected
794
+ ? 'rgba(220, 50, 50, 0.6)'
795
+ : 'rgba(200, 60, 60, 0.5)'
796
+ this.ctx.strokeStyle = isSelected ? '#ff3333' : '#cc4444'
797
+ } else if (accent === 'green') {
798
+ this.ctx.fillStyle = isSelected
799
+ ? 'rgba(0, 170, 92, 0.65)'
800
+ : 'rgba(0, 170, 92, 0.5)'
801
+ this.ctx.strokeStyle = isSelected ? '#00e676' : '#00aa5c'
802
+ } else {
803
+ this.ctx.fillStyle = isSelected ? 'rgba(250, 140, 22, 0.5)' : 'rgba(24, 144, 255, 0.5)'
804
+ this.ctx.strokeStyle = isSelected ? '#fa8c16' : '#1890ff'
805
+ }
806
+ this.ctx.lineWidth = 1
807
+ this.ctx.fillRect(x, labelY, labelWidth, labelHeight)
808
+ this.ctx.strokeRect(x, labelY, labelWidth, labelHeight)
809
+ this.ctx.fillStyle = '#ffffff'
810
+ this.ctx.fillText(labelText, x + labelPadding, textY)
811
+ },
812
+
813
+ // ========== 全屏相关方法 ==========
814
+ openFullscreen() {
815
+ this.isFullscreen = true
816
+ this.showAnnotations = true
817
+ this.fullscreenScale = 1
818
+ this.fullscreenTranslateX = 0
819
+ this.fullscreenTranslateY = 0
820
+ this.$nextTick(() => {
821
+ this.initFullscreenSize()
822
+ this.focusFullscreenContainer()
823
+ })
824
+ },
825
+ initFullscreenSize() {
826
+ const container = this.$refs.fullscreenContainer
827
+ if (!container) return
828
+
829
+ const containerWidth = container.clientWidth
830
+ const containerHeight = container.clientHeight
831
+ const img = this.$refs.fullscreenImage
832
+
833
+ if (!img || !img.naturalWidth) return
834
+
835
+ const maxW = containerWidth * 0.9
836
+ const maxH = containerHeight * 0.9
837
+ const scaleX = maxW / img.naturalWidth
838
+ const scaleY = maxH / img.naturalHeight
839
+ const fitScale = Math.min(scaleX, scaleY, 1)
840
+
841
+ this.fullscreenScale = fitScale
842
+ },
843
+ handleFullscreenImageLoad() {
844
+ this.$nextTick(() => {
845
+ this.initFullscreenSize()
846
+ this.updateFullscreenCanvasSize()
847
+ })
848
+ },
849
+ closeFullscreen() {
850
+ this.isFullscreen = false
851
+ this.fullscreenScale = 1
852
+ this.fullscreenTranslateX = 0
853
+ this.fullscreenTranslateY = 0
854
+ this.$emit('closeFullscreen')
855
+ },
856
+ updateFullscreenCanvasSize() {
857
+ const img = this.$refs.fullscreenImage
858
+ const canvas = this.$refs.fullscreenCanvas
859
+ if (!img || !canvas) return
860
+
861
+ const w = img.naturalWidth || img.width
862
+ const h = img.naturalHeight || img.height
863
+
864
+ if (w === 0 || h === 0) return
865
+
866
+ canvas.width = w
867
+ canvas.height = h
868
+ canvas.style.width = w + 'px'
869
+ canvas.style.height = h + 'px'
870
+
871
+ this.fullscreenCtx = canvas.getContext('2d')
872
+ this.drawFullscreenAnnotations()
873
+ },
874
+ drawFullscreenAnnotations() {
875
+ if (!this.fullscreenCtx || !this.$refs.fullscreenCanvas) return
876
+ const ctx = this.fullscreenCtx
877
+ const canvas = this.$refs.fullscreenCanvas
878
+
879
+ if (!this.showAnnotations) {
880
+ ctx.clearRect(0, 0, canvas.width, canvas.height)
881
+ return
882
+ }
883
+
884
+ ctx.clearRect(0, 0, canvas.width, canvas.height)
885
+
886
+ this.annotations.forEach((ann, index) => {
887
+ const type = this.getAnnotationType(ann)
888
+ const isSelected = index === this.localSelectedIndex
889
+ switch (type) {
890
+ case 'rect':
891
+ this.drawFullscreenRect(ctx, ann, index, isSelected)
892
+ break
893
+ case 'polygon':
894
+ this.drawFullscreenPolygon(ctx, ann, index, isSelected)
895
+ break
896
+ case 'line':
897
+ this.drawFullscreenLine(ctx, ann, index, isSelected)
898
+ break
899
+ case 'point':
900
+ this.drawFullscreenPoint(ctx, ann, index, isSelected)
901
+ break
902
+ case 'hrnet':
903
+ this.drawFullscreenHrnet(ctx, ann, index, isSelected)
904
+ break
905
+ }
906
+ })
907
+ },
908
+ drawFullscreenRect(ctx, ann, index, isSelected) {
909
+ const box = ann.box
910
+ if (!box) return
911
+ if (isSelected) {
912
+ ctx.strokeStyle = '#fa8c16'
913
+ ctx.fillStyle = 'rgba(250, 140, 22, 0.15)'
914
+ ctx.lineWidth = 2.5
915
+ } else {
916
+ ctx.strokeStyle = '#1890ff'
917
+ ctx.fillStyle = 'rgba(24, 144, 255, 0.08)'
918
+ ctx.lineWidth = 2
919
+ }
920
+ ctx.fillRect(box.x, box.y, box.width, box.height)
921
+ ctx.strokeRect(box.x, box.y, box.width, box.height)
922
+ this.drawFullscreenLabel(ctx, box.x, box.y, `${index + 1}. ${ann.objectName || '未命名'}`, ann.confidence, isSelected)
923
+ },
924
+ drawFullscreenPolygon(ctx, ann, index, isSelected) {
925
+ const mask = ann.mask
926
+ if (!mask || mask.length < 3) return
927
+ if (isSelected) {
928
+ ctx.strokeStyle = '#fa8c16'
929
+ ctx.fillStyle = 'rgba(250, 140, 22, 0.15)'
930
+ ctx.lineWidth = 2.5
931
+ } else {
932
+ ctx.strokeStyle = '#1890ff'
933
+ ctx.fillStyle = 'rgba(24, 144, 255, 0.08)'
934
+ ctx.lineWidth = 2
935
+ }
936
+ ctx.lineCap = 'round'
937
+ ctx.lineJoin = 'round'
938
+ ctx.beginPath()
939
+ ctx.moveTo(mask[0].x, mask[0].y)
940
+ for (let i = 1; i < mask.length; i++) {
941
+ ctx.lineTo(mask[i].x, mask[i].y)
942
+ }
943
+ ctx.closePath()
944
+ ctx.fill()
945
+ ctx.stroke()
946
+ const minX = Math.min(...mask.map(p => p.x))
947
+ const minY = Math.min(...mask.map(p => p.y))
948
+ this.drawFullscreenLabel(ctx, minX, minY, `${index + 1}. ${ann.objectName || '未命名'}`, ann.confidence, isSelected)
949
+ },
950
+ drawFullscreenLine(ctx, ann, index, isSelected) {
951
+ const line = ann.line || (ann.points && ann.points.length >= 2 ? { start: ann.points[0], end: ann.points[1] } : null)
952
+ if (!line || !line.start || !line.end) return
953
+ const color = isSelected ? '#fa8c16' : '#1890ff'
954
+ ctx.strokeStyle = color
955
+ ctx.fillStyle = color
956
+ ctx.lineWidth = isSelected ? 2.5 : 2
957
+ ctx.lineCap = 'round'
958
+ ctx.beginPath()
959
+ ctx.moveTo(line.start.x, line.start.y)
960
+ ctx.lineTo(line.end.x, line.end.y)
961
+ ctx.stroke()
962
+ ;[line.start, line.end].forEach(p => {
963
+ ctx.beginPath()
964
+ ctx.arc(p.x, p.y, 4, 0, Math.PI * 2)
965
+ ctx.fillStyle = '#ffffff'
966
+ ctx.fill()
967
+ ctx.stroke()
968
+ })
969
+ const midX = (line.start.x + line.end.x) / 2
970
+ const midY = (line.start.y + line.end.y) / 2
971
+ this.drawFullscreenLabel(ctx, midX, midY - 8, `${index + 1}. ${ann.objectName || '线段'}`, ann.confidence, isSelected)
972
+ },
973
+ drawFullscreenPoint(ctx, ann, index, isSelected) {
974
+ const points = this.getPointAnnotations(ann)
975
+ if (points.length === 0) return
976
+ const radius = isSelected ? 11 : 10
977
+ const centerAlpha = isSelected ? 0.9 : 0.5
978
+ const edgeAlpha = 0
979
+ points.forEach(p => {
980
+ const gradient = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, radius)
981
+ gradient.addColorStop(0, `rgba(255, 60, 60, ${centerAlpha})`)
982
+ gradient.addColorStop(1, `rgba(255, 60, 60, ${edgeAlpha})`)
983
+ ctx.beginPath()
984
+ ctx.arc(p.x, p.y, radius, 0, Math.PI * 2)
985
+ ctx.fillStyle = gradient
986
+ ctx.fill()
987
+ })
988
+ if (points.length > 0) {
989
+ const first = points[0]
990
+ this.drawFullscreenLabel(ctx, first.x + 10, first.y - 10, `${index + 1}. ${ann.objectName || '点'}`, ann.confidence, isSelected, 'red')
991
+ }
992
+ },
993
+ drawFullscreenHrnet(ctx, ann, index, isSelected) {
994
+ const centerlineGroups = this.getHrnetCenterlineGroups(ann)
995
+ const keypoints = this.getHrnetKeypoints(ann)
996
+
997
+ centerlineGroups.forEach(points => {
998
+ this.drawFullscreenHrnetCenterline(ctx, points, isSelected)
999
+ })
1000
+
1001
+ keypoints.forEach((point, pointIndex) => {
1002
+ this.drawFullscreenHrnetKeypoint(ctx, point, pointIndex, isSelected)
1003
+ })
1004
+
1005
+ const firstCenterlineGroup = centerlineGroups.find(points => points.length > 0)
1006
+ const labelPoint = keypoints[0] || (firstCenterlineGroup ? firstCenterlineGroup[0] : null)
1007
+ if (labelPoint) {
1008
+ this.drawFullscreenLabel(ctx, labelPoint.x + 12, labelPoint.y - 12, `${index + 1}. ${ann.objectName || ann.class_name || 'HRNet'}`, ann.confidence, isSelected, 'green')
1009
+ }
1010
+ },
1011
+ drawFullscreenHrnetCenterline(ctx, points, isSelected) {
1012
+ const sampledPoints = points.slice(0, 1800)
1013
+ if (points.length >= 2) {
1014
+ ctx.beginPath()
1015
+ ctx.moveTo(points[0].x, points[0].y)
1016
+ for (let i = 1; i < points.length; i++) {
1017
+ ctx.lineTo(points[i].x, points[i].y)
1018
+ }
1019
+ ctx.strokeStyle = isSelected ? 'rgba(250, 140, 22, 0.65)' : 'rgba(255, 190, 11, 0.32)'
1020
+ ctx.lineWidth = isSelected ? 3 : 2
1021
+ ctx.lineCap = 'round'
1022
+ ctx.lineJoin = 'round'
1023
+ ctx.stroke()
1024
+ }
1025
+ sampledPoints.forEach(point => {
1026
+ const score = Math.max(0, Math.min(1, Number(point.score || 0)))
1027
+ const radius = Math.max(1.8, 2.4 + score * 5.5) + (isSelected ? 1.2 : 0)
1028
+ ctx.beginPath()
1029
+ ctx.arc(point.x, point.y, radius, 0, Math.PI * 2)
1030
+ ctx.fillStyle = score > 0.55
1031
+ ? `rgba(255, 45, 26, ${Math.max(0.18, Math.min(0.58, 0.18 + score * 0.34))})`
1032
+ : `rgba(255, 190, 11, ${Math.max(0.12, Math.min(0.46, 0.12 + score * 0.34))})`
1033
+ ctx.fill()
1034
+ })
1035
+ },
1036
+ drawFullscreenHrnetKeypoint(ctx, point, index, isSelected) {
1037
+ const score = Math.max(0, Math.min(1, Number(point.score || 0)))
1038
+ const haloRadius = isSelected ? 15 : 13
1039
+ const dotRadius = isSelected ? 7 : 6
1040
+ ctx.beginPath()
1041
+ ctx.arc(point.x, point.y, haloRadius, 0, Math.PI * 2)
1042
+ ctx.fillStyle = 'rgba(0, 230, 118, 0.22)'
1043
+ ctx.fill()
1044
+ ctx.beginPath()
1045
+ ctx.arc(point.x, point.y, dotRadius, 0, Math.PI * 2)
1046
+ ctx.fillStyle = '#00e676'
1047
+ ctx.fill()
1048
+ ctx.strokeStyle = isSelected ? '#ffffff' : '#06351b'
1049
+ ctx.lineWidth = 2
1050
+ ctx.stroke()
1051
+ const labelText = `#${index + 1} ${(score * 100).toFixed(1)}%`
1052
+ ctx.font = 'bold 12px Arial'
1053
+ ctx.lineWidth = 3
1054
+ ctx.strokeStyle = '#06351b'
1055
+ ctx.fillStyle = '#ffffff'
1056
+ ctx.strokeText(labelText, point.x + 10, Math.max(14, point.y - 8))
1057
+ ctx.fillText(labelText, point.x + 10, Math.max(14, point.y - 8))
1058
+ },
1059
+ drawFullscreenLabel(ctx, x, y, text, confidence, isSelected, accent) {
1060
+ let labelText = text
1061
+ if (confidence !== undefined && confidence !== null) {
1062
+ labelText += ` (${(confidence * 100).toFixed(1)}%)`
1063
+ }
1064
+ const labelPadding = 6
1065
+ const labelHeight = 22
1066
+ ctx.font = 'bold 16px Arial'
1067
+ const labelWidth = ctx.measureText(labelText).width + labelPadding * 2
1068
+ const labelY = y >= labelHeight ? y - labelHeight : y
1069
+ const textY = y >= labelHeight ? y - 7 : y + 15
1070
+ if (accent === 'red') {
1071
+ ctx.fillStyle = isSelected ? 'rgba(220, 50, 50, 0.6)' : 'rgba(200, 60, 60, 0.5)'
1072
+ ctx.strokeStyle = isSelected ? '#ff3333' : '#cc4444'
1073
+ } else if (accent === 'green') {
1074
+ ctx.fillStyle = isSelected ? 'rgba(0, 170, 92, 0.65)' : 'rgba(0, 170, 92, 0.5)'
1075
+ ctx.strokeStyle = isSelected ? '#00e676' : '#00aa5c'
1076
+ } else {
1077
+ ctx.fillStyle = isSelected ? 'rgba(250, 140, 22, 0.5)' : 'rgba(24, 144, 255, 0.5)'
1078
+ ctx.strokeStyle = isSelected ? '#fa8c16' : '#1890ff'
1079
+ }
1080
+ ctx.lineWidth = 1
1081
+ ctx.fillRect(x, labelY, labelWidth, labelHeight)
1082
+ ctx.strokeRect(x, labelY, labelWidth, labelHeight)
1083
+ ctx.fillStyle = '#ffffff'
1084
+ ctx.fillText(labelText, x + labelPadding, textY)
1085
+ },
1086
+ toggleAnnotations() {
1087
+ this.showAnnotations = !this.showAnnotations
1088
+ if (this.showAnnotations) {
1089
+ this.drawFullscreenAnnotations()
1090
+ } else {
1091
+ const canvas = this.$refs.fullscreenCanvas
1092
+ if (canvas && this.fullscreenCtx) {
1093
+ this.fullscreenCtx.clearRect(0, 0, canvas.width, canvas.height)
1094
+ }
1095
+ }
1096
+ },
1097
+ handleFullscreenWheel(e) {
1098
+ e.preventDefault()
1099
+ const delta = e.deltaY > 0 ? -0.1 : 0.1
1100
+ this.fullscreenScale = Math.min(Math.max(0.3, this.fullscreenScale + delta), 3)
1101
+ requestAnimationFrame(() => {
1102
+ this.updateFullscreenCanvasSize()
1103
+ })
1104
+ },
1105
+ startFullscreenDrag(e) {
1106
+ if (e.button !== 0) return
1107
+ e.preventDefault()
1108
+ this.isFullscreenDragging = true
1109
+ this.fullscreenDragStartX = e.clientX - this.fullscreenTranslateX
1110
+ this.fullscreenDragStartY = e.clientY - this.fullscreenTranslateY
1111
+ document.addEventListener('mousemove', this.onFullscreenDrag)
1112
+ document.addEventListener('mouseup', this.endFullscreenDrag)
1113
+ },
1114
+ onFullscreenDrag(e) {
1115
+ if (!this.isFullscreenDragging) return
1116
+ this.fullscreenTranslateX = e.clientX - this.fullscreenDragStartX
1117
+ this.fullscreenTranslateY = e.clientY - this.fullscreenDragStartY
1118
+ },
1119
+ endFullscreenDrag() {
1120
+ if (!this.isFullscreenDragging) return
1121
+ this.isFullscreenDragging = false
1122
+ document.removeEventListener('mousemove', this.onFullscreenDrag)
1123
+ document.removeEventListener('mouseup', this.endFullscreenDrag)
1124
+ },
1125
+ focusFullscreenContainer() {
1126
+ if (this.$refs.fullscreenContainer) {
1127
+ this.$refs.fullscreenContainer.focus()
1128
+ }
1129
+ },
1130
+ handleFullscreenKeydown(e) {
1131
+ if (e.key === 'Escape') {
1132
+ e.preventDefault()
1133
+ e.stopPropagation()
1134
+ this.closeFullscreen()
1135
+ }
1136
+ },
1137
+ handleGlobalKeydown(e) {
1138
+ if (e.key === 'Escape' && this.isFullscreen) {
1139
+ e.preventDefault()
1140
+ e.stopPropagation()
1141
+ this.closeFullscreen()
1142
+ }
1143
+ }
1144
+ }
1145
+ }
1146
+ </script>
1147
+
1148
+ <style scoped>
1149
+ .image-annotation-show {
1150
+ position: relative;
1151
+ display: flex;
1152
+ flex-direction: column;
1153
+ width: 100%;
1154
+ height: 100%;
1155
+ min-height: 400px;
1156
+ background: #1a1d24;
1157
+ border-radius: 8px;
1158
+ overflow: hidden;
1159
+ }
1160
+
1161
+ /* 画布容器 */
1162
+ .canvas-container {
1163
+ position: relative;
1164
+ flex: 1;
1165
+ min-height: 0;
1166
+ overflow: auto;
1167
+ display: flex;
1168
+ align-items: flex-start;
1169
+ justify-content: center;
1170
+ padding: 5px 0;
1171
+ background: #1a1d24;
1172
+ }
1173
+
1174
+ .canvas-zoom-wrapper {
1175
+ transition: width 0.15s ease, height 0.15s ease;
1176
+ transform-origin: top left;
1177
+ }
1178
+
1179
+ .canvas-inner {
1180
+ position: relative;
1181
+ transform-origin: top left;
1182
+ transition: none;
1183
+ }
1184
+
1185
+ .canvas-inner.is-panning {
1186
+ cursor: grabbing !important;
1187
+ }
1188
+
1189
+ .annotation-image {
1190
+ display: block;
1191
+ user-select: none;
1192
+ transition: opacity 0.3s ease;
1193
+ }
1194
+
1195
+ .annotation-image.image-loading {
1196
+ opacity: 0.5;
1197
+ }
1198
+
1199
+ .annotation-canvas-layer {
1200
+ position: absolute;
1201
+ top: 0;
1202
+ left: 0;
1203
+ cursor: pointer;
1204
+ transition: opacity 0.3s ease;
1205
+ }
1206
+
1207
+ .annotation-canvas-layer.canvas-hidden {
1208
+ opacity: 0;
1209
+ }
1210
+
1211
+ /* 空状态 */
1212
+ .canvas-empty {
1213
+ position: absolute;
1214
+ top: 50%;
1215
+ left: 50%;
1216
+ transform: translate(-50%, -50%);
1217
+ color: #6b7280;
1218
+ font-size: 14px;
1219
+ text-align: center;
1220
+ }
1221
+
1222
+ /* 加载中 */
1223
+ .canvas-loading {
1224
+ position: absolute;
1225
+ top: 50%;
1226
+ left: 50%;
1227
+ transform: translate(-50%, -50%);
1228
+ }
1229
+
1230
+ /* 全屏按钮 */
1231
+ .fullscreen-btn {
1232
+ position: absolute;
1233
+ top: 12px;
1234
+ right: 12px;
1235
+ z-index: 10;
1236
+ background: rgba(0, 0, 0, 0.4) !important;
1237
+ border-color: rgba(255, 255, 255, 0.2) !important;
1238
+ color: #fff !important;
1239
+ transition: all 0.3s ease;
1240
+ }
1241
+
1242
+ .fullscreen-btn:hover {
1243
+ background: rgba(0, 0, 0, 0.6) !important;
1244
+ border-color: rgba(255, 255, 255, 0.4) !important;
1245
+ color: #40a9ff !important;
1246
+ }
1247
+
1248
+ /* 全屏预览容器 */
1249
+ .fullscreen-preview-wrapper {
1250
+ position: fixed;
1251
+ top: 0;
1252
+ left: 0;
1253
+ right: 0;
1254
+ bottom: 0;
1255
+ z-index: 10000;
1256
+ background: rgba(0, 0, 0, 0.85);
1257
+ display: flex;
1258
+ flex-direction: column;
1259
+ }
1260
+
1261
+ /* 全屏图片预览区域 */
1262
+ .fullscreen-preview-wrapper .image-preview-container {
1263
+ flex: 1;
1264
+ display: flex;
1265
+ justify-content: center;
1266
+ align-items: center;
1267
+ overflow: hidden;
1268
+ position: relative;
1269
+ cursor: grab;
1270
+ outline: none;
1271
+ }
1272
+
1273
+ .fullscreen-preview-wrapper .image-preview-container.dragging {
1274
+ cursor: grabbing;
1275
+ }
1276
+
1277
+ .fullscreen-preview-wrapper .image-wrapper {
1278
+ display: flex;
1279
+ justify-content: center;
1280
+ align-items: center;
1281
+ width: 100%;
1282
+ height: 100%;
1283
+ transition: transform 0.1s ease;
1284
+ will-change: transform;
1285
+ }
1286
+
1287
+ .fullscreen-preview-wrapper .image-inner {
1288
+ position: relative;
1289
+ display: inline-block;
1290
+ line-height: 0;
1291
+ font-size: 0;
1292
+ }
1293
+
1294
+ .fullscreen-preview-wrapper .preview-image-wrapper {
1295
+ position: relative;
1296
+ display: inline-block;
1297
+ line-height: 0;
1298
+ }
1299
+
1300
+ .fullscreen-preview-wrapper .preview-image {
1301
+ display: block;
1302
+ width: auto;
1303
+ height: auto;
1304
+ max-width: none;
1305
+ max-height: none;
1306
+ user-select: none;
1307
+ -webkit-user-drag: none;
1308
+ }
1309
+
1310
+ .fullscreen-canvas {
1311
+ position: absolute;
1312
+ top: 0;
1313
+ left: 0;
1314
+ pointer-events: none;
1315
+ transition: opacity 0.3s ease;
1316
+ }
1317
+
1318
+ .fullscreen-canvas.canvas-hidden {
1319
+ opacity: 0;
1320
+ }
1321
+
1322
+ /* 底部工具栏 */
1323
+ .fullscreen-preview-wrapper .bottom-toolbar {
1324
+ position: fixed;
1325
+ bottom: 40px;
1326
+ left: 0;
1327
+ right: 0;
1328
+ z-index: 1011;
1329
+ display: flex;
1330
+ justify-content: center;
1331
+ pointer-events: none;
1332
+ }
1333
+
1334
+ .fullscreen-preview-wrapper .toolbar-content {
1335
+ display: flex;
1336
+ align-items: center;
1337
+ gap: 8px;
1338
+ background: rgba(0, 0, 0, 0.6);
1339
+ backdrop-filter: blur(8px);
1340
+ border-radius: 24px;
1341
+ padding: 10px 20px;
1342
+ pointer-events: auto;
1343
+ border: 1px solid rgba(255, 255, 255, 0.15);
1344
+ box-shadow: 0 6px 16px rgba(0, 0, 0, 0.3);
1345
+ }
1346
+
1347
+ .fullscreen-preview-wrapper .toolbar-divider {
1348
+ width: 1px;
1349
+ height: 20px;
1350
+ background: rgba(255, 255, 255, 0.2);
1351
+ margin: 0 4px;
1352
+ }
1353
+
1354
+ .fullscreen-preview-wrapper .toolbar-btn {
1355
+ background: rgba(255, 255, 255, 0.15);
1356
+ border-color: rgba(255, 255, 255, 0.2);
1357
+ color: #fff;
1358
+ transition: all 0.3s ease;
1359
+ }
1360
+
1361
+ .fullscreen-preview-wrapper .toolbar-btn:hover {
1362
+ background: rgba(255, 255, 255, 0.25);
1363
+ border-color: rgba(255, 255, 255, 0.3);
1364
+ color: #40a9ff;
1365
+ transform: translateY(-2px);
1366
+ }
1367
+
1368
+ .fullscreen-preview-wrapper .toolbar-btn.close-btn:hover {
1369
+ color: #ff4d4f;
1370
+ }
1371
+ </style>