vue2-client 1.22.32 → 1.22.33

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.
@@ -6,99 +6,247 @@
6
6
  @click.self="closePreview"
7
7
  ref="previewContainer"
8
8
  >
9
+ <!-- 顶部栏 -->
10
+ <div class="top-bar">
11
+ <div class="top-left"></div>
12
+ <div class="top-center">
13
+ <!-- 影像胶片按钮 -->
14
+ <a-button
15
+ type="primary"
16
+ :style="{
17
+ backgroundColor: !compareMode ? '#0057FE' : 'transparent',
18
+ color: !compareMode ? 'white' : '#aaa',
19
+ borderBottomRightRadius: '0',
20
+ borderBottomLeftRadius: '0',
21
+ border: '0'
9
22
 
10
- <a-row>
11
- <a-col :span="4">
12
- </a-col>
13
- <a-col :span="16" class="preview-toolbar" style="padding-bottom: 0px; border-bottom: solid #0057FE;">
14
- <a-button type="primary" style="background-color: #0057FE; color:white ;">影像胶片</a-button>
15
- </a-col>
16
- <a-col :span="4">
17
- <!-- 顶部功能区 -->
18
- <div class="preview-toolbar">
19
- <button @click="zoomIn" title="放大">+</button>
20
- <button @click="zoomOut" title="缩小">-</button>
21
- <button @click="rotateLeft" title="左旋转">↺</button>
22
- <button @click="rotateRight" title="右旋转">↻</button>
23
- <button @click="resetTransform" title="重置">↻</button>
24
- <button @click="closePreview" title="关闭" class="close-btn">×</button>
23
+ }"
24
+ @click="switchToFilm"
25
+ >
26
+ 影像胶片
27
+ </a-button>
28
+
29
+ <!-- 影像对比按钮(仅当有对比数据时显示) -->
30
+ <a-button
31
+ v-if="hasCompare"
32
+ type="primary"
33
+ :style="{
34
+ backgroundColor: compareMode ? '#0057FE' : 'transparent',
35
+ color: compareMode ? 'white' : '#aaa',
36
+ borderBottomRightRadius: '0',
37
+ borderBottomLeftRadius: '0',
38
+ border: '0'
39
+ }"
40
+ @click="switchToCompare"
41
+ >
42
+ 影像对比
43
+ </a-button>
44
+ </div>
45
+ <div class="top-right">
46
+ <!-- 功能按钮:对比模式下隐藏但保留占位 -->
47
+ <button
48
+ v-for="(btn, idx) in funcButtons"
49
+ :key="idx"
50
+ :style="{ visibility: compareMode ? 'hidden' : 'visible', pointerEvents: compareMode ? 'none' : 'auto' }"
51
+ @click="btn.handler"
52
+ :title="btn.title"
53
+ >{{ btn.label }}</button>
54
+ <!-- 关闭按钮 -->
55
+ <button @click="closePreview" title="关闭" class="close-btn">×</button>
56
+ </div>
57
+ </div>
58
+
59
+ <!-- 对比模式下的左右操作栏 -->
60
+ <div v-if="compareMode" class="compare-toolbar">
61
+ <div class="compare-panel">
62
+ <div class="compare-panel-title">当前胶片</div>
63
+ <a-date-picker
64
+ show-time
65
+ size="small"
66
+ class="compare-date-picker"
67
+ :value="leftDate"
68
+ disabled
69
+ >
70
+ <template #renderExtraFooter>extra footer</template>
71
+ </a-date-picker>
72
+ <span v-if="getFilmTime('left')" class="film-time">{{ getFilmTime('left') }}</span>
73
+ <button class="pacs-btn" @click="handlePacsSync('left')">PACS同步查看胶片</button>
74
+ <div class="toolbar-group left-toolbar">
75
+ <button @click="zoomIn('left')" title="放大">+</button>
76
+ <button @click="zoomOut('left')" title="缩小">-</button>
77
+ <button @click="rotateLeft('left')" title="左旋转">↺</button>
78
+ <button @click="rotateRight('left')" title="右旋转">↻</button>
79
+ <button @click="resetTransform('left')" title="重置">↻</button>
80
+ </div>
81
+ </div>
82
+ <div class="compare-panel">
83
+ <div class="compare-panel-title">历史胶片</div>
84
+ <a-date-picker
85
+ show-time
86
+ size="small"
87
+ class="compare-date-picker"
88
+ :value="rightDate"
89
+ disabled
90
+ >
91
+ <template #renderExtraFooter>extra footer</template>
92
+ </a-date-picker>
93
+ <span v-if="getFilmTime('right')" class="film-time">{{ getFilmTime('right') }}</span>
94
+ <button class="pacs-btn" @click="handlePacsSync('right')">PACS同步查看胶片</button>
95
+ <div class="toolbar-group right-toolbar">
96
+ <button @click="zoomIn('right')" title="放大">+</button>
97
+ <button @click="zoomOut('right')" title="缩小">-</button>
98
+ <button @click="rotateLeft('right')" title="左旋转">↺</button>
99
+ <button @click="rotateRight('right')" title="右旋转">↻</button>
100
+ <button @click="resetTransform('right')" title="重置">↻</button>
25
101
  </div>
26
- </a-col>
27
- </a-row>
28
- <!-- 主图片区域 -->
29
- <div
30
- class="preview-content"
31
- @mousedown="startDrag"
32
- @mousemove="onDrag"
33
- @mouseup="stopDrag"
34
- @touchstart="handleTouchStart"
35
- @touchmove="handleTouchMove"
36
- @touchend="stopDrag"
37
- >
102
+ </div>
103
+ </div>
104
+
105
+ <!-- 主内容区域 -->
106
+ <!-- 非对比模式 -->
107
+ <div v-if="!compareMode" class="preview-content single-mode" ref="previewContent">
38
108
  <div v-if="isLoading" class="loading-indicator">加载中...</div>
39
109
  <img
40
- :src="currentImage"
41
- :style="imageStyle"
110
+ :src="currentLeftImage"
111
+ :style="getImageStyle('left')"
42
112
  class="preview-image"
43
- @wheel.prevent="handleWheel"
113
+ @wheel.prevent="handleWheel($event, 'left')"
114
+ @mousedown="startDrag($event, 'left')"
115
+ @mousemove="onDrag($event, 'left')"
116
+ @mouseup="stopDrag('left')"
117
+ @touchstart="handleTouchStart($event, 'left')"
118
+ @touchmove="handleTouchMove($event, 'left')"
119
+ @touchend="stopDrag('left')"
44
120
  @load="isLoading = false"
45
121
  @error="handleImageError"
46
122
  />
123
+ <div class="thumbnail-navigation single-thumb">
124
+ <div
125
+ v-for="(item, index) in imgList"
126
+ :key="index"
127
+ :class="['thumbnail-nav-item', { active: leftIndex === index }]"
128
+ @click="switchLeftImage(index)"
129
+ >
130
+ <img :src="getImageUrl(item)" class="thumbnail-nav-image" alt="缩略图" />
131
+ </div>
132
+ </div>
133
+ <div class="thumbnail-description">
134
+ <p>当前查看第<span class="thumbnail-description-span">{{ leftIndex + 1 }}</span>张胶片,共<span class="thumbnail-description-span">{{ imgList.length }}</span>张胶片</p>
135
+ </div>
47
136
  </div>
48
137
 
49
- <!-- 底部缩略图导航 -->
50
- <div class="thumbnail-navigation">
51
- <div
52
- v-for="(img, index) in imgList"
53
- :key="index"
54
- :class="['thumbnail-nav-item', { active: currentIndex === index }]"
55
- @click="switchImage(index)"
56
- >
57
- <img :src="img" class="thumbnail-nav-image" alt="缩略图" v-lazy="img" />
138
+ <!-- 对比模式 -->
139
+ <div v-else class="compare-wrapper">
140
+ <div class="compare-side left-side" ref="compareLeft">
141
+ <div class="compare-image-wrapper">
142
+ <img
143
+ :src="currentLeftImage"
144
+ :style="getImageStyle('left')"
145
+ class="preview-image compare-image"
146
+ @wheel.prevent="handleWheel($event, 'left')"
147
+ @mousedown="startDrag($event, 'left')"
148
+ @mousemove="onDrag($event, 'left')"
149
+ @mouseup="stopDrag('left')"
150
+ @touchstart="handleTouchStart($event, 'left')"
151
+ @touchmove="handleTouchMove($event, 'left')"
152
+ @touchend="stopDrag('left')"
153
+ @load="isLoading = false"
154
+ @error="handleImageError"
155
+ />
156
+ </div>
157
+ <div class="thumbnail-navigation side-thumb">
158
+ <div
159
+ v-for="(item, index) in imgList"
160
+ :key="'l' + index"
161
+ :class="['thumbnail-nav-item', { active: leftIndex === index }]"
162
+ @click="switchLeftImage(index)"
163
+ >
164
+ <img :src="getImageUrl(item)" class="thumbnail-nav-image" alt="缩略图" />
165
+ </div>
166
+ </div>
167
+ <div class="side-description">
168
+ 当前查看第 <span class="thumbnail-description-span">{{ leftIndex + 1 }}</span> 张胶片,共 {{ imgList.length }} 张胶片
169
+ </div>
170
+ </div>
171
+
172
+ <div class="compare-side right-side" ref="compareRight">
173
+ <div class="compare-image-wrapper">
174
+ <img
175
+ :src="currentRightImage"
176
+ :style="getImageStyle('right')"
177
+ class="preview-image compare-image"
178
+ @wheel.prevent="handleWheel($event, 'right')"
179
+ @mousedown="startDrag($event, 'right')"
180
+ @mousemove="onDrag($event, 'right')"
181
+ @mouseup="stopDrag('right')"
182
+ @touchstart="handleTouchStart($event, 'right')"
183
+ @touchmove="handleTouchMove($event, 'right')"
184
+ @touchend="stopDrag('right')"
185
+ @load="isLoading = false"
186
+ @error="handleImageError"
187
+ />
188
+ </div>
189
+ <div class="thumbnail-navigation side-thumb">
190
+ <div
191
+ v-for="(item, index) in compareList"
192
+ :key="'r' + index"
193
+ :class="['thumbnail-nav-item', { active: rightIndex === index }]"
194
+ @click="switchRightImage(index)"
195
+ >
196
+ <img :src="getImageUrl(item)" class="thumbnail-nav-image" alt="缩略图" />
197
+ </div>
198
+ </div>
199
+ <div class="side-description">
200
+ 当前查看第 <span class="thumbnail-description-span">{{ rightIndex + 1 }}</span> 张胶片,共 {{ compareList.length }} 张胶片
201
+ </div>
58
202
  </div>
59
- </div>
60
- <div class="thumbnail-description">
61
- <p>当前查看第<span class="thumbnail-description-sapn">{{ currentIndex + 1 }}</span>张胶片,共<span class="thumbnail-description-sapn">{{ imgList.length }}</span>张胶片</p>
62
203
  </div>
63
204
  </div>
64
205
  </transition>
65
206
  </template>
66
207
 
67
208
  <script>
68
-
69
209
  export default {
70
210
  name: 'ImagePreview',
71
211
  props: {
72
212
  visible: { type: Boolean, default: false },
73
213
  imgList: { type: Array, default: () => [] },
214
+ compareList: { type: Array, default: () => [] },
74
215
  initialIndex: { type: Number, default: 0 }
75
216
  },
76
217
  data () {
77
218
  return {
78
- currentIndex: this.initialIndex,
79
- scale: 1,
80
- rotate: 0,
81
- position: { x: 0, y: 0 },
82
- isDragging: false,
83
- startDragPosition: { x: 0, y: 0 },
84
- touchStartX: 0,
85
- touchStartY: 0,
86
- initialDistance: null,
219
+ leftIndex: this.initialIndex,
220
+ rightIndex: this.initialIndex,
221
+ leftState: { scale: 1, rotate: 0, pos: { x: 0, y: 0 } },
222
+ rightState: { scale: 1, rotate: 0, pos: { x: 0, y: 0 } },
223
+ leftDrag: { isDragging: false, startPos: { x: 0, y: 0 }, maxX: 0, maxY: 0 },
224
+ rightDrag: { isDragging: false, startPos: { x: 0, y: 0 }, maxX: 0, maxY: 0 },
225
+ touchData: { left: { initialDist: null }, right: { initialDist: null } },
87
226
  isLoading: true,
88
- maxDragX: 0,
89
- maxDragY: 0
227
+ compareMode: false,
228
+ leftDate: null,
229
+ rightDate: null
90
230
  }
91
231
  },
92
232
  computed: {
93
- currentImage () {
94
- return this.imgList[this.currentIndex] || ''
233
+ hasCompare () {
234
+ return this.compareList && this.compareList.length > 0
95
235
  },
96
- imageStyle () {
97
- return {
98
- transform: `scale(${this.scale}) rotate(${this.rotate}deg) translate(${this.position.x}px, ${this.position.y}px)`,
99
- transition: this.isDragging ? 'none' : 'transform 0.3s ease',
100
- cursor: this.isDragging ? 'grabbing' : 'grab'
101
- }
236
+ currentLeftImage () {
237
+ return this.getImageUrl(this.imgList[this.leftIndex] || '')
238
+ },
239
+ currentRightImage () {
240
+ return this.getImageUrl(this.compareList[this.rightIndex] || '')
241
+ },
242
+ funcButtons () {
243
+ return [
244
+ { label: '+', title: '放大', handler: () => this.zoomIn('left') },
245
+ { label: '-', title: '缩小', handler: () => this.zoomOut('left') },
246
+ { label: '↺', title: '左旋转', handler: () => this.rotateLeft('left') },
247
+ { label: '↻', title: '右旋转', handler: () => this.rotateRight('left') },
248
+ { label: '↻', title: '重置', handler: () => this.resetTransform('left') }
249
+ ]
102
250
  }
103
251
  },
104
252
  watch: {
@@ -106,159 +254,258 @@ export default {
106
254
  if (newVal) {
107
255
  document.body.style.overflow = 'hidden'
108
256
  document.addEventListener('keydown', this.handleKeyDown)
109
- // 计算最大拖拽边界
110
- this.$nextTick(() => {
111
- const container = this.$refs.previewContainer
112
- if (container) {
113
- this.maxDragX = (container.clientWidth * this.scale - container.clientWidth) / 2
114
- this.maxDragY = (container.clientHeight * this.scale - container.clientHeight) / 2
115
- }
116
- })
257
+ this.$nextTick(() => this.updateAllMaxDrag())
117
258
  } else {
118
259
  document.body.style.overflow = ''
119
260
  document.removeEventListener('keydown', this.handleKeyDown)
120
261
  }
121
262
  },
122
- scale (newScale) {
123
- // 缩放时更新拖拽边界
124
- this.$nextTick(() => {
125
- const container = this.$refs.previewContainer
126
- if (container) {
127
- this.maxDragX = (container.clientWidth * newScale - container.clientWidth) / 2
128
- this.maxDragY = (container.clientHeight * newScale - container.clientHeight) / 2
129
- // 确保图片位置在边界内
130
- this.position.x = Math.max(-this.maxDragX, Math.min(this.maxDragX, this.position.x))
131
- this.position.y = Math.max(-this.maxDragY, Math.min(this.maxDragY, this.position.y))
132
- }
133
- })
263
+ compareMode () {
264
+ this.$nextTick(() => this.updateAllMaxDrag())
134
265
  }
135
266
  },
136
267
  methods: {
137
- // 变换控制
138
- zoomIn () {
139
- this.scale = Math.min(5, this.scale + 0.2)
268
+ // ---------- 模式切换(新增) ----------
269
+ switchToFilm () {
270
+ if (!this.compareMode) return // 已经是胶片模式,不操作
271
+ this.compareMode = false
272
+ this.resetTransform('left')
273
+ this.resetTransform('right')
274
+ // 如有需要可重置右侧索引,但保持原样
275
+ this.$nextTick(() => this.updateAllMaxDrag())
276
+ },
277
+ switchToCompare () {
278
+ if (this.compareMode) return // 已经是对比模式,不操作
279
+ this.compareMode = true
280
+ this.resetTransform('left')
281
+ this.resetTransform('right')
282
+ this.leftDate = this.getUploadDate('left')
283
+ this.rightDate = this.getUploadDate('right')
284
+ if (this.rightIndex >= this.compareList.length) {
285
+ this.rightIndex = 0
286
+ }
287
+ this.$nextTick(() => this.updateAllMaxDrag())
288
+ },
289
+ // ---------- 原有方法 ----------
290
+ getImageUrl (item) {
291
+ if (!item) return ''
292
+ if (typeof item === 'string') return item
293
+ if (typeof item === 'object' && item.url) return item.url
294
+ return ''
295
+ },
296
+ getState (side) {
297
+ return side === 'left' ? this.leftState : this.rightState
298
+ },
299
+ getDragState (side) {
300
+ return side === 'left' ? this.leftDrag : this.rightDrag
140
301
  },
141
- zoomOut () {
142
- this.scale = Math.max(0.2, this.scale - 0.2)
302
+ getTouchData (side) {
303
+ return this.touchData[side]
143
304
  },
144
- rotateLeft () {
145
- this.rotate -= 90
305
+ getImageStyle (side) {
306
+ const state = this.getState(side)
307
+ const drag = this.getDragState(side)
308
+ return {
309
+ transform: `scale(${state.scale}) rotate(${state.rotate}deg) translate(${state.pos.x}px, ${state.pos.y}px)`,
310
+ transition: drag.isDragging ? 'none' : 'transform 0.3s ease',
311
+ cursor: drag.isDragging ? 'grabbing' : 'grab'
312
+ }
146
313
  },
147
- rotateRight () {
148
- this.rotate += 90
314
+
315
+ applyZoom (side, delta) {
316
+ const state = this.getState(side)
317
+ const prevScale = state.scale
318
+ const newScale = Math.max(0.2, Math.min(5, prevScale + delta))
319
+ if (newScale === prevScale) return
320
+ const ratio = newScale / prevScale
321
+ state.pos.x *= ratio
322
+ state.pos.y *= ratio
323
+ state.scale = newScale
324
+ this.$nextTick(() => this.updateMaxDrag(side))
149
325
  },
150
- resetTransform () {
151
- this.scale = 1
152
- this.rotate = 0
153
- this.position = { x: 0, y: 0 }
326
+ zoomIn (side) { this.applyZoom(side, 0.2) },
327
+ zoomOut (side) { this.applyZoom(side, -0.2) },
328
+
329
+ rotateSide (side, deg) {
330
+ this.getState(side).rotate += deg
154
331
  },
332
+ rotateLeft (side) { this.rotateSide(side, -90) },
333
+ rotateRight (side) { this.rotateSide(side, 90) },
155
334
 
156
- // 图片导航
157
- switchImage (index) {
158
- this.currentIndex = index
159
- this.resetTransform()
160
- this.isLoading = true // 切换图片时显示加载状态
335
+ resetTransform (side) {
336
+ const state = this.getState(side)
337
+ state.scale = 1
338
+ state.rotate = 0
339
+ state.pos = { x: 0, y: 0 }
340
+ this.$nextTick(() => this.updateMaxDrag(side))
161
341
  },
162
- prevImage () {
163
- this.currentIndex = (this.currentIndex - 1 + this.imgList.length) % this.imgList.length
164
- this.resetTransform()
342
+
343
+ switchLeftImage (index) {
344
+ this.leftIndex = index
345
+ this.resetTransform('left')
346
+ this.leftDate = this.getUploadDate('left')
347
+ this.isLoading = true
165
348
  },
166
- nextImage () {
167
- this.currentIndex = (this.currentIndex + 1) % this.imgList.length
168
- this.resetTransform()
349
+ switchRightImage (index) {
350
+ this.rightIndex = index
351
+ this.resetTransform('right')
352
+ this.rightDate = this.getUploadDate('right')
353
+ this.isLoading = true
169
354
  },
170
355
 
171
- // 拖拽控制
172
- startDrag (e) {
173
- this.isDragging = true
356
+ startDrag (e, side) {
357
+ const drag = this.getDragState(side)
358
+ drag.isDragging = true
174
359
  const clientX = e.clientX || e.touches?.[0]?.clientX
175
360
  const clientY = e.clientY || e.touches?.[0]?.clientY
176
- this.startDragPosition = {
177
- x: clientX - this.position.x,
178
- y: clientY - this.position.y
361
+ const state = this.getState(side)
362
+ drag.startPos = {
363
+ x: clientX - state.pos.x,
364
+ y: clientY - state.pos.y
179
365
  }
180
366
  },
181
- onDrag (e) {
182
- if (!this.isDragging || e.touches?.length > 1) return
367
+ onDrag (e, side) {
368
+ const drag = this.getDragState(side)
369
+ if (!drag.isDragging || e.touches?.length > 1) return
183
370
  const clientX = e.clientX || e.touches?.[0]?.clientX
184
371
  const clientY = e.clientY || e.touches?.[0]?.clientY
185
372
  if (clientX === undefined || clientY === undefined) return
186
- const newX = clientX - this.startDragPosition.x
187
- const newY = clientY - this.startDragPosition.y
188
- // 限制拖拽边界
189
- this.position = {
190
- x: Math.max(-this.maxDragX, Math.min(this.maxDragX, newX)),
191
- y: Math.max(-this.maxDragY, Math.min(this.maxDragY, newY))
373
+ const state = this.getState(side)
374
+ const newX = clientX - drag.startPos.x
375
+ const newY = clientY - drag.startPos.y
376
+ state.pos = {
377
+ x: Math.max(-drag.maxX, Math.min(drag.maxX, newX)),
378
+ y: Math.max(-drag.maxY, Math.min(drag.maxY, newY))
192
379
  }
193
380
  },
194
- stopDrag () {
195
- this.isDragging = false
196
- this.initialDistance = null // 重置双指距离
381
+ stopDrag (side) {
382
+ this.getDragState(side).isDragging = false
383
+ this.getTouchData(side).initialDist = null
197
384
  },
198
385
 
199
- // 触摸支持
200
- handleTouchStart (e) {
386
+ handleTouchStart (e, side) {
201
387
  if (e.touches.length === 1) {
202
- this.touchStartX = e.touches[0].clientX
203
- this.touchStartY = e.touches[0].clientY
204
- this.startDrag(e)
388
+ this.startDrag(e, side)
205
389
  } else if (e.touches.length === 2) {
206
- // 双指触摸开始,记录初始距离
207
- this.initialDistance = Math.hypot(
390
+ const touch = this.getTouchData(side)
391
+ touch.initialDist = Math.hypot(
208
392
  e.touches[1].clientX - e.touches[0].clientX,
209
393
  e.touches[1].clientY - e.touches[0].clientY
210
394
  )
211
395
  }
212
396
  },
213
- handleTouchMove (e) {
397
+ handleTouchMove (e, side) {
214
398
  if (e.touches.length === 1) {
215
- // 单指拖动
216
- this.onDrag(e)
217
- } else if (e.touches.length === 2 && this.initialDistance) {
218
- // 双指缩放
399
+ this.onDrag(e, side)
400
+ } else if (e.touches.length === 2) {
401
+ const touch = this.getTouchData(side)
402
+ if (!touch.initialDist) return
219
403
  e.preventDefault()
220
- const currentDistance = Math.hypot(
404
+ const currentDist = Math.hypot(
221
405
  e.touches[1].clientX - e.touches[0].clientX,
222
406
  e.touches[1].clientY - e.touches[0].clientY
223
407
  )
224
- const scaleFactor = currentDistance / this.initialDistance
225
- this.scale = Math.max(0.2, Math.min(5, this.scale * scaleFactor))
226
- this.initialDistance = currentDistance // 更新距离基准
408
+ const scaleFactor = currentDist / touch.initialDist
409
+ const state = this.getState(side)
410
+ const newScale = Math.max(0.2, Math.min(5, state.scale * scaleFactor))
411
+ const ratio = newScale / state.scale
412
+ state.pos.x *= ratio
413
+ state.pos.y *= ratio
414
+ state.scale = newScale
415
+ touch.initialDist = currentDist
416
+ this.$nextTick(() => this.updateMaxDrag(side))
417
+ }
418
+ },
419
+
420
+ handleWheel (e, side) {
421
+ e.preventDefault()
422
+ const delta = e.deltaY > 0 ? -0.1 : 0.1
423
+ this.applyZoom(side, delta)
424
+ },
425
+
426
+ updateMaxDrag (side) {
427
+ const container = this.$refs.previewContainer
428
+ if (!container) return
429
+ const state = this.getState(side)
430
+ const drag = this.getDragState(side)
431
+ let el = null
432
+ if (this.compareMode) {
433
+ el = side === 'left' ? this.$refs.compareLeft : this.$refs.compareRight
434
+ } else {
435
+ el = this.$refs.previewContent
227
436
  }
437
+ if (!el) el = container
438
+ const rect = el.getBoundingClientRect()
439
+ const w = rect.width
440
+ const h = rect.height
441
+ drag.maxX = Math.max(0, (w * state.scale - w) / 2)
442
+ drag.maxY = Math.max(0, (h * state.scale - h) / 2)
443
+ state.pos.x = Math.max(-drag.maxX, Math.min(drag.maxX, state.pos.x))
444
+ state.pos.y = Math.max(-drag.maxY, Math.min(drag.maxY, state.pos.y))
445
+ },
446
+ updateAllMaxDrag () {
447
+ this.updateMaxDrag('left')
448
+ this.updateMaxDrag('right')
228
449
  },
229
450
 
230
- // 键盘支持
231
451
  handleKeyDown (e) {
232
452
  switch (e.key) {
233
453
  case 'Escape': this.closePreview(); break
234
454
  case 'ArrowLeft':
235
- e.shiftKey ? this.rotateLeft() : this.prevImage()
455
+ if (this.compareMode) {
456
+ e.shiftKey ? this.rotateLeft('left') : this.switchLeftImage(Math.max(0, this.leftIndex - 1))
457
+ } else {
458
+ e.shiftKey ? this.rotateLeft('left') : this.switchLeftImage(Math.max(0, this.leftIndex - 1))
459
+ }
236
460
  break
237
461
  case 'ArrowRight':
238
- e.shiftKey ? this.rotateRight() : this.nextImage()
462
+ if (this.compareMode) {
463
+ e.shiftKey ? this.rotateRight('left') : this.switchLeftImage(Math.min(this.imgList.length - 1, this.leftIndex + 1))
464
+ } else {
465
+ e.shiftKey ? this.rotateRight('left') : this.switchLeftImage(Math.min(this.imgList.length - 1, this.leftIndex + 1))
466
+ }
239
467
  break
240
- case '+': this.zoomIn(); break
241
- case '-': this.zoomOut(); break
242
- case 'r': this.resetTransform(); break
468
+ case '+': this.zoomIn('left'); break
469
+ case '-': this.zoomOut('left'); break
470
+ case 'r': this.resetTransform('left'); break
243
471
  }
244
472
  },
245
473
 
246
- // 滚轮缩放
247
- handleWheel (e) {
248
- e.preventDefault()
249
- const delta = e.deltaY > 0 ? -0.1 : 0.1
250
- this.scale = Math.max(0.2, Math.min(5, this.scale + delta))
251
- },
252
-
253
- // 图片加载失败处理
254
474
  handleImageError () {
255
- console.error('图片加载失败:', this.currentImage)
475
+ console.error('图片加载失败')
256
476
  this.isLoading = false
257
477
  },
258
478
 
259
- // 关闭预览
260
479
  closePreview () {
261
480
  this.$emit('close')
481
+ },
482
+ getFilmTime (side) {
483
+ const list = side === 'left' ? this.imgList : this.compareList
484
+ const index = side === 'left' ? this.leftIndex : this.rightIndex
485
+ const item = list[index]
486
+ if (!item || typeof item !== 'object') return ''
487
+ return item.filmTime || item.imageTime || item.createTime || ''
488
+ },
489
+ getUploadDate (side) {
490
+ const list = side === 'left' ? this.imgList : this.compareList
491
+ const index = side === 'left' ? this.leftIndex : this.rightIndex
492
+ const item = list[index]
493
+ if (!item || typeof item !== 'object') return null
494
+ return item.f_upload_date || null
495
+ },
496
+ handlePacsSync (side) {
497
+ const eventName = 'pacs-sync-view'
498
+ if (this.$listeners[eventName]) {
499
+ let image = {}
500
+ if(side === 'left') {
501
+ image = this.imgList[this.leftIndex]
502
+ } else {
503
+ image = this.compareList[this.rightIndex]
504
+ }
505
+ this.$emit(eventName, { side, image })
506
+ } else {
507
+ this.$message?.warn?.(`未实现事件函数 [${eventName}]`)
508
+ }
262
509
  }
263
510
  },
264
511
  beforeUnmount () {
@@ -268,6 +515,7 @@ export default {
268
515
  </script>
269
516
 
270
517
  <style scoped>
518
+ /* 样式未变,保持原样 */
271
519
  /* 过渡动画 */
272
520
  .fade-enter-active, .fade-leave-active {
273
521
  transition: opacity 0.3s;
@@ -290,78 +538,264 @@ export default {
290
538
  touch-action: none; /* 禁用浏览器默认触摸行为 */
291
539
  }
292
540
 
293
- /* 顶部工具栏 */
294
- .preview-toolbar {
295
- padding: 15px;
541
+ /* 顶部栏 */
542
+ .top-bar {
296
543
  display: flex;
544
+ align-items: center;
545
+ padding: 10px 16px;
546
+ padding-bottom:0px ;
547
+ background: rgba(0,0,0,0.4);
548
+ flex-shrink: 0;
549
+ }
550
+ .top-left {
551
+ width: 15%;
552
+ }
553
+ .top-center {
554
+ flex: 1;
555
+ display: flex;
556
+ margin-left: 20px;
557
+ margin-right: 20px;
558
+ border-bottom: 2px solid #0057FE;
297
559
  justify-content: center;
298
- gap: 4px;
299
- z-index: 1001;
300
- flex-wrap: wrap;
560
+ align-items: center;
561
+ gap: 10px;
562
+ }
563
+ .top-center .ant-btn {
564
+ font-weight: 500;
565
+ }
566
+ .top-right {
567
+ width: 15%;
568
+ justify-content: right;
569
+ display: flex;
570
+ align-items: center;
571
+ gap: 6px;
572
+ margin-left: auto;
301
573
  }
302
- .preview-toolbar button {
303
- padding: 2px 10px;
304
- min-width: 24px;
305
- min-height: 24px;
306
- margin: 0px;
307
- background: #E5E9F0;
574
+ .top-right button {
575
+ background: white;
308
576
  color: #5D5C5C;
309
577
  border: none;
310
578
  border-radius: 4px;
311
579
  cursor: pointer;
312
- font-size: 18px;
580
+ font-size: 14px;
581
+ min-width: 25px;
582
+ height: 25px;
313
583
  transition: background 0.3s;
314
584
  }
315
- .preview-toolbar button:hover {
585
+ .top-right button:hover {
316
586
  color: #3362DA;
317
587
  }
318
- .close-btn {
588
+ .top-right .close-btn {
319
589
  color: #94979E !important;
590
+ font-size: 24px;
591
+ padding: 0 6px;
592
+ width: 48px;
593
+ height: 48px;
594
+ border-radius: 50%;
595
+ background-color: black;
596
+ }
597
+ .top-right .close-btn:hover {
598
+ color: #fff !important;
599
+ }
600
+
601
+ /* 对比模式操作栏 */
602
+ .compare-toolbar {
603
+ display: flex;
604
+ flex-wrap: nowrap;
605
+ justify-content: space-between;
606
+ align-items: stretch;
607
+ padding: 8px 16px;
608
+ background: rgba(0,0,0,0.3);
609
+ border-bottom: 1px solid rgba(255,255,255,0.1);
610
+ flex-shrink: 0;
611
+ gap: 12px;
612
+ }
613
+ .compare-toolbar > div {
614
+ flex: 1;
615
+ min-width: 0;
616
+ display: flex;
617
+ flex-direction: row;
618
+ gap: 8px;
619
+ }
620
+ .toolbar-group {
621
+ display: flex;
622
+ flex-wrap: wrap;
623
+ align-items: center;
624
+ gap: 6px;
625
+ justify-content: flex-end;
626
+ margin-right: 0;
627
+ }
628
+ .film-time {
629
+ color: #9E9E9E;
630
+ font-size: 12px;
631
+ margin-left: 8px;
632
+ }
633
+ .toolbar-group button {
634
+ background: white;
635
+ color: #5D5C5C;
636
+ border: none;
637
+ border-radius: 4px;
638
+ cursor: pointer;
639
+ font-size: 14px;
640
+ min-width: 25px;
641
+ height: 25px;
642
+ transition: background 0.3s;
643
+ box-sizing: border-box;
644
+ line-height: 1;
645
+ }
646
+ .toolbar-group button:hover {
647
+ color: #3362DA;
320
648
  }
321
649
 
322
- /* 主图片区域 */
323
- .preview-content {
650
+ .compare-panel {
651
+ display: flex;
652
+ flex-direction: row;
653
+ align-items: center;
654
+ gap: 10px;
655
+ min-height: 0;
656
+ width: 100%;
657
+ flex-wrap: nowrap;
658
+ }
659
+ .compare-panel-title {
660
+ font-weight: 500;
661
+ color: #ddd;
662
+ white-space: nowrap;
663
+ overflow: hidden;
664
+ text-overflow: ellipsis;
665
+ min-width: 0;
666
+ max-width: 120px;
667
+ flex-shrink: 0;
668
+ }
669
+ .compare-panel .film-time {
670
+ flex-shrink: 0;
671
+ }
672
+ .compare-panel .toolbar-group {
673
+ margin-left: auto;
674
+ }
675
+ .compare-panel .compare-date-picker {
676
+ flex-shrink: 0;
677
+ }
678
+ .compare-panel .compare-date-picker .ant-input {
679
+ background: rgba(255, 255, 255, 0.08);
680
+ color: #ddd;
681
+ cursor: default;
682
+ }
683
+ .pacs-btn {
684
+ background: white;
685
+ color: #5D5C5C;
686
+ border: none;
687
+ border-radius: 4px;
688
+ cursor: pointer;
689
+ font-size: 13px;
690
+ line-height: 22px;
691
+ white-space: nowrap;
692
+ }
693
+ .pacs-btn:hover {
694
+ color: #3362DA;
695
+ }
696
+
697
+ /* 非对比模式 */
698
+ .preview-content.single-mode {
324
699
  flex: 1;
325
700
  display: flex;
701
+ flex-direction: column;
326
702
  justify-content: center;
327
703
  align-items: center;
328
704
  overflow: hidden;
329
705
  position: relative;
330
706
  }
331
- .preview-image {
707
+ .single-mode .preview-image {
332
708
  max-width: 95%;
333
- max-height: 80vh;
709
+ max-height: 65vh;
334
710
  object-fit: contain;
335
711
  transform-origin: center center;
336
- will-change: transform; /* 优化动画性能 */
712
+ will-change: transform;
713
+ user-select: none;
337
714
  }
338
- .loading-indicator {
339
- position: absolute;
340
- color: white;
341
- font-size: 1.2rem;
342
- z-index: 10;
715
+ .single-mode .thumbnail-navigation.single-thumb {
716
+ padding: 12px 0 6px 0;
717
+ display: flex;
718
+ justify-content: center;
719
+ overflow-x: auto;
720
+ gap: 10px;
721
+ width: 100%;
722
+ flex-wrap: nowrap;
723
+ }
724
+ .single-mode .thumbnail-description {
725
+ color: #9E9E9E;
726
+ text-align: center;
727
+ padding: 2px 0 10px 0;
728
+ font-size: 14px;
729
+ margin: 0;
730
+ }
731
+
732
+ /* 对比模式 */
733
+ .compare-wrapper {
734
+ flex: 1;
735
+ display: flex;
736
+ flex-direction: row;
737
+ overflow: hidden;
738
+ min-height: 0;
739
+ padding-bottom: 20px;
740
+ }
741
+ .compare-side {
742
+ flex: 1;
743
+ display: flex;
744
+ flex-direction: column;
745
+ overflow: hidden;
746
+ min-height: 0;
747
+ position: relative;
748
+ }
749
+ .left-side {
750
+ border-right: 1px solid rgba(255,255,255,0.12);
751
+ }
752
+ .compare-image-wrapper {
753
+ flex: 1;
754
+ display: flex;
755
+ justify-content: center;
756
+ align-items: center;
757
+ overflow: hidden;
758
+ position: relative;
759
+ min-height: 0;
343
760
  }
344
761
 
345
- /* 底部缩略图导航 */
346
- .thumbnail-navigation {
762
+ .compare-image-wrapper .preview-image.compare-image {
763
+ max-width: 92%;
764
+ max-height: 60vh;
765
+ object-fit: contain;
766
+ transform-origin: center center;
767
+ will-change: transform;
768
+ user-select: none;
769
+ }
770
+
771
+ .thumbnail-navigation.side-thumb {
347
772
  display: flex;
348
773
  justify-content: center;
349
- padding: 15px 0;
350
774
  overflow-x: auto;
351
- gap: 10px;
775
+ gap: 8px;
776
+ padding: 8px 10px 4px 10px;
777
+ margin-right: 40px;
778
+ margin-left: 40px;
779
+ flex-wrap: nowrap;
780
+ flex-shrink: 0;
781
+ background: rgba(0,0,0,0.2);
782
+ min-height: 70px;
783
+ align-items: center;
352
784
  }
785
+
353
786
  .thumbnail-nav-item {
354
- width: 60px;
355
- height: 60px;
787
+ width: 50px;
788
+ height: 50px;
356
789
  border: 2px solid transparent;
357
790
  border-radius: 4px;
358
791
  overflow: hidden;
359
792
  cursor: pointer;
360
- opacity: 0.6;
793
+ opacity: 0.5;
361
794
  transition: all 0.3s;
362
795
  flex-shrink: 0;
363
796
  }
364
- .thumbnail-nav-item:hover, .thumbnail-nav-item.active {
797
+ .thumbnail-nav-item:hover,
798
+ .thumbnail-nav-item.active {
365
799
  opacity: 1;
366
800
  border-color: #1890ff;
367
801
  }
@@ -370,24 +804,90 @@ export default {
370
804
  height: 100%;
371
805
  object-fit: cover;
372
806
  }
373
- /* 底部描述 */
374
- .thumbnail-description {
807
+
808
+ .side-description {
375
809
  color: #9E9E9E;
376
- margin: auto;
810
+ text-align: center;
811
+ font-size: 13px;
812
+ padding: 2px 0 8px 0;
813
+ flex-shrink: 0;
814
+ background: rgba(0,0,0,0.15);
377
815
  }
378
- .thumbnail-description-sapn {
816
+ .thumbnail-description-span {
379
817
  color: #3362DA;
818
+ font-weight: bold;
819
+ }
820
+
821
+ .loading-indicator {
822
+ position: absolute;
823
+ color: white;
824
+ font-size: 1.2rem;
825
+ z-index: 10;
380
826
  }
381
827
 
382
- /* 移动端优化 */
383
828
  @media (max-width: 768px) {
384
- .preview-toolbar {
385
- padding: 10px;
386
- gap: 8px;
829
+ .top-bar {
830
+ padding: 6px 10px;
831
+ flex-wrap: wrap;
832
+ }
833
+ .top-left {
834
+ flex: 0 0 10%;
835
+ }
836
+ .top-center {
837
+ gap: 6px;
838
+ font-size: 14px;
387
839
  }
388
- .preview-toolbar button {
389
- padding: 10px 16px;
840
+ .top-center .ant-btn {
841
+ font-size: 14px;
842
+ padding: 0 12px;
843
+ height: 30px;
844
+ }
845
+ .top-right button {
390
846
  font-size: 16px;
847
+ padding: 2px 6px;
848
+ min-width: 26px;
849
+ height: 26px;
850
+ }
851
+ .top-right .close-btn {
852
+ font-size: 20px;
853
+ }
854
+ .compare-toolbar {
855
+ padding: 4px 8px;
856
+ gap: 6px;
857
+ }
858
+ .toolbar-group button {
859
+ font-size: 14px;
860
+ padding: 2px 6px;
861
+ min-width: 24px;
862
+ height: 24px;
863
+ }
864
+ .compare-image-wrapper .preview-image.compare-image {
865
+ max-width: 88%;
866
+ max-height: 50vh;
867
+ }
868
+ .thumbnail-nav-item {
869
+ width: 40px;
870
+ height: 40px;
871
+ }
872
+ .thumbnail-navigation.side-thumb {
873
+ gap: 6px;
874
+ padding: 6px 6px 2px 6px;
875
+ min-height: 56px;
876
+ }
877
+ .side-description {
878
+ font-size: 11px;
879
+ padding: 1px 0 6px 0;
880
+ }
881
+ .single-mode .preview-image {
882
+ max-height: 55vh;
883
+ }
884
+ .single-mode .thumbnail-navigation.single-thumb {
885
+ gap: 6px;
886
+ padding: 8px 0 4px 0;
887
+ }
888
+ .single-mode .thumbnail-description {
889
+ font-size: 12px;
890
+ padding: 0 0 6px 0;
391
891
  }
392
892
  }
393
893
  </style>