vue2-client 1.22.21 → 1.22.23

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 (87) hide show
  1. package/.env.his +19 -19
  2. package/.idea/.name +1 -0
  3. package/.idea/MarsCodeWorkspaceAppSettings.xml +8 -0
  4. package/.idea/deployment.xml +14 -0
  5. package/.idea/gradle.xml +7 -0
  6. package/.idea/inspectionProfiles/Project_Default.xml +6 -0
  7. package/.idea/libraries/contour_plot.xml +9 -0
  8. package/.idea/material_theme_project_new.xml +18 -0
  9. package/.idea/misc.xml +87 -5
  10. package/package.json +1 -1
  11. package/src/base-client/components/common/HIS/HForm/HForm.vue +1186 -1186
  12. package/src/base-client/components/common/HIS/HTab/HTab.vue +790 -790
  13. package/src/base-client/components/common/XInspectionDetailDrawer/components/AiDeviceStatus.vue +25 -4
  14. package/src/base-client/components/common/XInspectionDetailDrawer/components/ImageAnnotationShow.vue +1371 -0
  15. package/src/base-client/components/common/XMarkdownViewer/demo.vue +102 -102
  16. package/src/base-client/components/his/XTransfer/index.md +327 -327
  17. package/.history/.eslintrc_20260521171150.js +0 -74
  18. package/.history/.eslintrc_20260521171213.js +0 -74
  19. package/.history/src/base-client/components/common/HIS/HAddNativeForm/HAddNativeForm_20260601154443.vue +0 -726
  20. package/.history/src/base-client/components/common/HIS/HAddNativeForm/HAddNativeForm_20260601154700.vue +0 -478
  21. package/.history/src/base-client/components/common/HIS/HButtons/HButtons_20260512175435.vue +0 -706
  22. package/.history/src/base-client/components/common/HIS/HButtons/HButtons_20260512175450.vue +0 -694
  23. package/.history/src/base-client/components/common/HIS/HButtons/HButtons_20260611152602.vue +0 -755
  24. package/.history/src/base-client/components/common/HIS/HForm/HForm_20260513145941.vue +0 -524
  25. package/.history/src/base-client/components/common/HIS/HForm/HForm_20260513153133.vue +0 -731
  26. package/.history/src/base-client/components/common/HIS/HForm/HForm_20260513160316.vue +0 -525
  27. package/.history/src/base-client/components/common/HIS/HForm/HForm_20260601144150.vue +0 -1046
  28. package/.history/src/base-client/components/common/HIS/HFormTable/HFormTable_20260310142713.vue +0 -512
  29. package/.history/src/base-client/components/common/HIS/HFormTable/HFormTable_20260310145118.vue +0 -511
  30. package/.history/src/base-client/components/common/HIS/HFormTable/HFormTable_20260311094834.vue +0 -696
  31. package/.history/src/base-client/components/common/HIS/HFormTable/HFormTable_20260320143028.vue +0 -693
  32. package/.history/src/base-client/components/common/HIS/HFormTable/HFormTable_20260409101450.vue +0 -677
  33. package/.history/src/base-client/components/common/HIS/HTab/HTab_20260625173703.vue +0 -767
  34. package/.history/src/base-client/components/common/HIS/HTab/HTab_20260626112425.vue +0 -803
  35. package/.history/src/base-client/components/common/XAddNativeForm/XAddNativeForm_20260513140637.vue +0 -1455
  36. package/.history/src/base-client/components/common/XAddNativeForm/XAddNativeForm_20260513140935.vue +0 -1441
  37. package/.history/src/base-client/components/common/XAddNativeForm/XAddNativeForm_20260513150818.vue +0 -1441
  38. package/.history/src/base-client/components/common/XAddNativeForm/XAddNativeForm_20260513153119.vue +0 -1442
  39. package/.history/src/base-client/components/common/XAddNativeForm/XAddNativeForm_20260513153126.vue +0 -1486
  40. package/.history/src/base-client/components/common/XForm/XFormItem_20260513140854.vue +0 -1607
  41. package/.history/src/base-client/components/common/XMarkdownViewer/XMarkdownViewer_20260519140403.vue +0 -643
  42. package/.history/src/base-client/components/common/XMarkdownViewer/XMarkdownViewer_20260519140829.vue +0 -628
  43. package/.history/src/base-client/components/common/XMarkdownViewer/demo_20260519142824.vue +0 -104
  44. package/.history/src/base-client/components/common/XMarkdownViewer/demo_20260519143155.vue +0 -102
  45. package/.history/src/base-client/components/common/XReportGrid/XReport_20260309171231.vue +0 -1241
  46. package/.history/src/base-client/components/common/XReportGrid/XReport_20260309171441.vue +0 -1223
  47. package/.history/src/base-client/components/his/HAi/HAi_20260612174826.vue +0 -472
  48. package/.history/src/base-client/components/his/HAi/HAi_20260612175839.vue +0 -538
  49. package/.history/src/base-client/components/his/HAi/HAi_20260615103331.vue +0 -650
  50. package/.history/src/base-client/components/his/HAi/HAi_20260618140214.vue +0 -1396
  51. package/.history/src/base-client/components/his/HAi/HAi_20260618140556.vue +0 -1396
  52. package/.history/src/base-client/components/his/XHDescriptions/XHDescriptions_20260424134504.vue +0 -1469
  53. package/.history/src/base-client/components/his/XSidebar/XSidebar_20260610171133.vue +0 -788
  54. package/.history/src/base-client/components/his/XSidebar/XSidebar_20260610171151.vue +0 -780
  55. package/.history/src/base-client/components/his/XTransfer/XTransfer_20260511170841.vue +0 -585
  56. package/.history/src/base-client/components/his/XTransfer/XTransfer_20260511171138.vue +0 -787
  57. package/.history/src/base-client/components/his/XTransfer/XTransfer_20260512141830.vue +0 -739
  58. package/.history/src/components/STable/index_20260409155138.js +0 -806
  59. package/.history/src/components/STable/index_20260409155218.js +0 -814
  60. package/.history/src/expression/core/Expression_20260305164427.js +0 -1371
  61. package/.history/src/expression/core/Expression_20260305170258.js +0 -1358
  62. package/.history/src/expression/core/Program_20260305111830.js +0 -944
  63. package/.history/src/expression/core/Program_20260305112041.js +0 -931
  64. package/.history/src/logic/LogicRunner_20260304154306.js +0 -170
  65. package/.history/src/logic/LogicRunner_20260304155553.js +0 -112
  66. package/.history/src/logic/LogicRunner_20260305105834.js +0 -112
  67. package/.history/src/logic/LogicRunner_20260305112718.js +0 -129
  68. package/.history/src/logic/LogicRunner_20260305182436.js +0 -133
  69. package/.history/src/logic/LogicRunner_20260306151301.js +0 -213
  70. package/.history/src/logic/LogicRunner_20260306152419.js +0 -213
  71. package/.history/src/logic/plugins/common/DateTools_20260305154159.js +0 -61
  72. package/.history/src/logic/plugins/common/DateTools_20260305154217.js +0 -44
  73. package/.history/src/logic/plugins/common/DateTools_20260305161014.js +0 -44
  74. package/.history/src/logic/plugins/common/HttpTools_20260305164352.js +0 -80
  75. package/.history/src/logic/plugins/common/HttpTools_20260305170258.js +0 -75
  76. package/.history/src/logic/plugins/common/HttpTools_20260305171634.js +0 -75
  77. package/.history/src/logic/plugins/common/HttpTools_20260306152419.js +0 -72
  78. package/.history/src/services/api/restTools_20260427142149.js +0 -245
  79. package/.history/src/services/api/restTools_20260427142853.js +0 -230
  80. package/.history/src/services/api/restTools_20260519135558.js +0 -230
  81. package/.history/src/services/api/restTools_20260519140825.js +0 -230
  82. package/.history/src/services/api/restTools_20260519151223.js +0 -230
  83. package/.history/src/utils/indexedDB_20260306150918.js +0 -593
  84. package/.history/src/utils/indexedDB_20260306151301.js +0 -586
  85. package/.idea/codeStyles/Project.xml +0 -62
  86. package/.idea/codeStyles/codeStyleConfig.xml +0 -5
  87. 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>