vue2-client 1.18.45 → 1.18.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vue2-client",
3
- "version": "1.18.45",
3
+ "version": "1.18.46",
4
4
  "private": false,
5
5
  "scripts": {
6
6
  "serve": "SET NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service serve --no-eslint",
@@ -17,52 +17,22 @@
17
17
  <div class="preview-toolbar top-toolbar">
18
18
  <div class="toolbar-content">
19
19
  <a-tooltip title="放大">
20
- <a-button
21
- type="primary"
22
- icon="zoom-in"
23
- @click="zoomIn"
24
- size="small"
25
- class="toolbar-btn"
26
- />
20
+ <a-button type="primary" icon="zoom-in" @click="zoomIn" size="small" class="toolbar-btn" />
27
21
  </a-tooltip>
28
22
  <a-tooltip title="缩小">
29
- <a-button
30
- type="primary"
31
- icon="zoom-out"
32
- @click="zoomOut"
33
- size="small"
34
- class="toolbar-btn"
35
- />
23
+ <a-button type="primary" icon="zoom-out" @click="zoomOut" size="small" class="toolbar-btn" />
36
24
  </a-tooltip>
37
25
  <a-tooltip title="重置">
38
- <a-button
39
- type="primary"
40
- icon="reload"
41
- @click="resetImageTransform"
42
- size="small"
43
- class="toolbar-btn"
44
- />
26
+ <a-button type="primary" icon="reload" @click="resetImageTransform" size="small" class="toolbar-btn" />
45
27
  </a-tooltip>
46
28
  <div class="zoom-display">
47
29
  <span class="zoom-percentage">{{ Math.round(scale * 100) }}%</span>
48
30
  </div>
49
31
  <a-tooltip title="向左旋转">
50
- <a-button
51
- type="primary"
52
- icon="undo"
53
- @click="rotateLeft"
54
- size="small"
55
- class="toolbar-btn"
56
- />
32
+ <a-button type="primary" icon="undo" @click="rotateLeft" size="small" class="toolbar-btn" />
57
33
  </a-tooltip>
58
34
  <a-tooltip title="向右旋转">
59
- <a-button
60
- type="primary"
61
- icon="redo"
62
- @click="rotateRight"
63
- size="small"
64
- class="toolbar-btn"
65
- />
35
+ <a-button type="primary" icon="redo" @click="rotateRight" size="small" class="toolbar-btn" />
66
36
  </a-tooltip>
67
37
  <a-tooltip title="下载图片">
68
38
  <a-button
@@ -71,6 +41,7 @@
71
41
  @click="downloadImage"
72
42
  size="small"
73
43
  class="toolbar-btn"
44
+ :loading="downloading"
74
45
  />
75
46
  </a-tooltip>
76
47
  </div>
@@ -81,24 +52,33 @@
81
52
  class="image-preview-container"
82
53
  @wheel="handleWheel"
83
54
  @mousedown="startDrag"
84
- @mousemove="onDrag"
85
- @mouseup="endDrag"
86
- @mouseleave="endDrag"
87
55
  ref="previewContainer"
88
- :class="{ 'dragging': isDragging }"
56
+ :class="{ dragging: isDragging }"
89
57
  tabindex="0"
90
58
  @keydown="handleKeydown"
91
59
  >
92
- <div
93
- class="image-wrapper"
94
- :style="wrapperStyle"
95
- >
60
+ <!-- 加载中状态 -->
61
+ <div v-if="loading" class="loading-wrapper">
62
+ <a-spin size="large" tip="图片加载中..." />
63
+ </div>
64
+
65
+ <!-- 加载失败状态 -->
66
+ <div v-else-if="loadError" class="error-wrapper">
67
+ <a-icon type="picture" class="error-icon" />
68
+ <p>图片加载失败</p>
69
+ <a-button type="link" @click="retryLoad">点击重试</a-button>
70
+ </div>
71
+
72
+ <!-- 图片内容 -->
73
+ <div v-show="!loading && !loadError" class="image-wrapper" :style="wrapperStyle">
96
74
  <img
97
75
  :src="imageUrl"
98
76
  alt="预览图片"
99
77
  class="preview-image"
100
78
  :style="imageStyle"
101
79
  ref="previewImage"
80
+ @load="onImageLoad"
81
+ @error="onImageError"
102
82
  />
103
83
  </div>
104
84
  </div>
@@ -106,6 +86,16 @@
106
86
  </template>
107
87
 
108
88
  <script>
89
+ // 缩放配置常量
90
+ const ZOOM_CONFIG = {
91
+ MIN: 0.1, // 最小缩放比例 10%
92
+ MAX: 3, // 最大缩放比例 300%
93
+ STEP: 0.25 // 统一缩放步长
94
+ }
95
+
96
+ // 旋转配置常量
97
+ const ROTATE_STEP = 90
98
+
109
99
  export default {
110
100
  name: 'ImagePreviewModal',
111
101
  props: {
@@ -125,7 +115,7 @@ export default {
125
115
  default: 'preview_image'
126
116
  }
127
117
  },
128
- data () {
118
+ data() {
129
119
  return {
130
120
  scale: 1, // 缩放比例
131
121
  rotate: 0, // 旋转角度
@@ -134,22 +124,20 @@ export default {
134
124
  dragStartY: 0, // 拖动起始Y坐标
135
125
  translateX: 0, // X轴平移距离
136
126
  translateY: 0, // Y轴平移距离
137
- lastTranslateX: 0, // 上一次X轴平移距离
138
- lastTranslateY: 0 // 上一次Y轴平移距离
127
+ loading: true, // 图片加载中
128
+ loadError: false, // 图片加载失败
129
+ downloading: false // 下载中状态
139
130
  }
140
131
  },
141
132
  computed: {
142
- // 图片样式
143
- imageStyle () {
133
+ // 图片样式(仅处理缩放和旋转)
134
+ imageStyle() {
144
135
  return {
145
- transform: `scale(${this.scale}) rotate(${this.rotate}deg)`,
146
- maxWidth: '100%',
147
- maxHeight: '100%',
148
- objectFit: 'contain'
136
+ transform: `scale(${this.scale}) rotate(${this.rotate}deg)`
149
137
  }
150
138
  },
151
139
  // 图片容器样式
152
- wrapperStyle () {
140
+ wrapperStyle() {
153
141
  return {
154
142
  transform: `translate(${this.translateX}px, ${this.translateY}px)`,
155
143
  cursor: this.isDragging ? 'grabbing' : 'grab'
@@ -157,50 +145,46 @@ export default {
157
145
  }
158
146
  },
159
147
  watch: {
160
- // 监听visible变化,重置状态和焦点管理
161
- visible (newVal) {
148
+ // 监听visible变化,重置状态
149
+ visible(newVal) {
162
150
  if (newVal) {
163
151
  this.resetImageTransform()
164
- // 下一个tick确保DOM已更新
152
+ this.loading = true
153
+ this.loadError = false
165
154
  this.$nextTick(() => {
166
155
  this.focusContainer()
167
156
  })
168
- } else {
169
- this.removeKeydownListener()
170
157
  }
158
+ },
159
+ // 监听图片URL变化,重新加载
160
+ imageUrl() {
161
+ this.loading = true
162
+ this.loadError = false
171
163
  }
172
164
  },
173
- mounted () {
165
+ mounted() {
174
166
  // 组件挂载时添加全局键盘事件监听
175
- this.addKeydownListener()
167
+ document.addEventListener('keydown', this.handleGlobalKeydown)
176
168
  },
177
- beforeDestroy () {
178
- // 组件销毁前移除事件监听
179
- this.removeKeydownListener()
169
+ beforeDestroy() {
170
+ // 组件销毁前移除所有事件监听(防止内存泄漏)
171
+ document.removeEventListener('keydown', this.handleGlobalKeydown)
172
+ document.removeEventListener('mousemove', this.onDrag)
173
+ document.removeEventListener('mouseup', this.endDrag)
180
174
  },
181
175
  methods: {
182
- // 添加键盘事件监听
183
- addKeydownListener () {
184
- document.addEventListener('keydown', this.handleGlobalKeydown)
185
- },
186
-
187
- // 移除键盘事件监听
188
- removeKeydownListener () {
189
- document.removeEventListener('keydown', this.handleGlobalKeydown)
190
- },
191
-
192
- // 处理全局键盘事件(ESC键)
193
- handleGlobalKeydown (event) {
194
- if (event.keyCode === 27 && this.visible) { // ESC键
176
+ // 处理全局键盘事件(使用 event.key 替代废弃的 keyCode)
177
+ handleGlobalKeydown(event) {
178
+ if (event.key === 'Escape' && this.visible) {
195
179
  event.preventDefault()
196
180
  event.stopPropagation()
197
181
  this.handleClose()
198
182
  }
199
183
  },
200
184
 
201
- // 处理容器内键盘事件(备用)
202
- handleKeydown (event) {
203
- if (event.keyCode === 27) { // ESC键
185
+ // 处理容器内键盘事件
186
+ handleKeydown(event) {
187
+ if (event.key === 'Escape') {
204
188
  event.preventDefault()
205
189
  event.stopPropagation()
206
190
  this.handleClose()
@@ -208,104 +192,170 @@ export default {
208
192
  },
209
193
 
210
194
  // 聚焦到预览容器
211
- focusContainer () {
195
+ focusContainer() {
212
196
  if (this.$refs.previewContainer) {
213
197
  this.$refs.previewContainer.focus()
214
198
  }
215
199
  },
216
200
 
217
- // 放大
218
- zoomIn () {
219
- this.scale = Math.min(this.scale + 0.25, 3) // 最大放大到300%
201
+ // 图片加载完成
202
+ onImageLoad() {
203
+ this.loading = false
204
+ this.loadError = false
205
+ },
206
+
207
+ // 图片加载失败
208
+ onImageError() {
209
+ this.loading = false
210
+ this.loadError = true
211
+ },
212
+
213
+ // 重试加载图片
214
+ retryLoad() {
215
+ this.loading = true
216
+ this.loadError = false
217
+ // 通过添加时间戳强制重新加载
218
+ const img = this.$refs.previewImage
219
+ if (img) {
220
+ const separator = this.imageUrl.includes('?') ? '&' : '?'
221
+ img.src = `${this.imageUrl}${separator}_t=${Date.now()}`
222
+ }
223
+ },
224
+
225
+ // 放大(同时回归居中)
226
+ zoomIn() {
227
+ this.scale = Math.min(this.scale + ZOOM_CONFIG.STEP, ZOOM_CONFIG.MAX)
228
+ this.centerImage()
229
+ },
230
+
231
+ // 缩小(同时回归居中)
232
+ zoomOut() {
233
+ this.scale = Math.max(this.scale - ZOOM_CONFIG.STEP, ZOOM_CONFIG.MIN)
234
+ this.centerImage()
220
235
  },
221
236
 
222
- // 缩小
223
- zoomOut () {
224
- this.scale = Math.max(this.scale - 0.25, 0.1) // 最小缩小到10%
237
+ // 图片回归居中
238
+ centerImage() {
239
+ this.translateX = 0
240
+ this.translateY = 0
225
241
  },
226
242
 
227
- // 向左旋转
228
- rotateLeft () {
229
- this.rotate -= 90
243
+ // 向左旋转(同时回归居中)
244
+ rotateLeft() {
245
+ this.rotate -= ROTATE_STEP
246
+ this.centerImage()
230
247
  },
231
248
 
232
- // 向右旋转
233
- rotateRight () {
234
- this.rotate += 90
249
+ // 向右旋转(同时回归居中)
250
+ rotateRight() {
251
+ this.rotate += ROTATE_STEP
252
+ this.centerImage()
235
253
  },
236
254
 
237
255
  // 重置图片变换
238
- resetImageTransform () {
256
+ resetImageTransform() {
239
257
  this.scale = 1
240
258
  this.rotate = 0
241
259
  this.translateX = 0
242
260
  this.translateY = 0
243
- this.lastTranslateX = 0
244
- this.lastTranslateY = 0
245
261
  },
246
262
 
247
- // 鼠标滚轮缩放
248
- handleWheel (event) {
263
+ // 鼠标滚轮缩放(放大以鼠标为中心,缩小居中)
264
+ handleWheel(event) {
249
265
  event.preventDefault()
250
- const delta = event.deltaY > 0 ? -0.1 : 0.1
251
- const newScale = Math.min(Math.max(0.1, this.scale + delta), 3)
252
-
253
- // 计算缩放中心点
254
- const rect = this.$refs.previewContainer.getBoundingClientRect()
255
- const mouseX = event.clientX - rect.left
256
- const mouseY = event.clientY - rect.top
257
-
258
- // 计算缩放前后的位置偏移,实现以鼠标位置为中心的缩放
259
- const scaleRatio = newScale / this.scale
260
- this.translateX = mouseX - (mouseX - this.translateX) * scaleRatio
261
- this.translateY = mouseY - (mouseY - this.translateY) * scaleRatio
266
+ const delta = event.deltaY > 0 ? -ZOOM_CONFIG.STEP : ZOOM_CONFIG.STEP
267
+ const newScale = Math.min(Math.max(ZOOM_CONFIG.MIN, this.scale + delta), ZOOM_CONFIG.MAX)
268
+
269
+ // 判断是放大还是缩小
270
+ const isZoomIn = newScale > this.scale
271
+
272
+ if (isZoomIn) {
273
+ // 放大时:始终以鼠标位置为中心缩放
274
+ const rect = this.$refs.previewContainer.getBoundingClientRect()
275
+ const mouseX = event.clientX - rect.left
276
+ const mouseY = event.clientY - rect.top
277
+ const scaleRatio = newScale / this.scale
278
+ this.translateX = mouseX - (mouseX - this.translateX) * scaleRatio
279
+ this.translateY = mouseY - (mouseY - this.translateY) * scaleRatio
280
+ } else {
281
+ // 缩小时:回归居中
282
+ this.centerImage()
283
+ }
262
284
 
263
285
  this.scale = newScale
264
286
  },
265
287
 
266
- // 开始拖动
267
- startDrag (event) {
268
- if (event.button !== 0) return // 只响应左键拖动
288
+ // 开始拖动(鼠标按下)
289
+ startDrag(event) {
290
+ // 只响应左键拖动
291
+ if (event.button !== 0) return
292
+ // 防止选中文字等默认行为
293
+ event.preventDefault()
269
294
  this.isDragging = true
270
295
  this.dragStartX = event.clientX - this.translateX
271
296
  this.dragStartY = event.clientY - this.translateY
297
+ // 添加全局鼠标事件监听,确保在容器外松开也能停止拖动
298
+ document.addEventListener('mousemove', this.onDrag)
299
+ document.addEventListener('mouseup', this.endDrag)
272
300
  },
273
301
 
274
302
  // 拖动中
275
- onDrag (event) {
303
+ onDrag(event) {
276
304
  if (!this.isDragging) return
277
305
  this.translateX = event.clientX - this.dragStartX
278
306
  this.translateY = event.clientY - this.dragStartY
279
307
  },
280
308
 
281
- // 结束拖动
282
- endDrag () {
309
+ // 结束拖动(鼠标松开)
310
+ endDrag() {
311
+ if (!this.isDragging) return
283
312
  this.isDragging = false
284
- this.lastTranslateX = this.translateX
285
- this.lastTranslateY = this.translateY
313
+ // 移除全局鼠标事件监听
314
+ document.removeEventListener('mousemove', this.onDrag)
315
+ document.removeEventListener('mouseup', this.endDrag)
286
316
  },
287
317
 
288
- // 下载图片
289
- downloadImage () {
290
- if (!this.imageUrl) return
291
-
292
- // 创建下载链接
293
- const link = document.createElement('a')
294
- link.href = this.imageUrl
295
- link.download = `${this.imageName}_${new Date().getTime()}.${this.getImageExtension(this.imageUrl)}`
296
- document.body.appendChild(link)
297
- link.click()
298
- document.body.removeChild(link)
318
+ // 下载图片(支持跨域)
319
+ async downloadImage() {
320
+ if (!this.imageUrl || this.downloading) return
321
+
322
+ this.downloading = true
323
+ try {
324
+ // 尝试通过 fetch 下载(支持跨域)
325
+ const response = await fetch(this.imageUrl, { mode: 'cors' })
326
+ if (!response.ok) throw new Error('下载失败')
327
+
328
+ const blob = await response.blob()
329
+ const blobUrl = URL.createObjectURL(blob)
330
+ const link = document.createElement('a')
331
+ link.href = blobUrl
332
+ link.download = `${this.imageName}_${Date.now()}.${this.getImageExtension(this.imageUrl)}`
333
+ document.body.appendChild(link)
334
+ link.click()
335
+ document.body.removeChild(link)
336
+ URL.revokeObjectURL(blobUrl)
337
+ } catch {
338
+ // 降级方案:直接使用 a 标签下载(可能会打开新窗口)
339
+ const link = document.createElement('a')
340
+ link.href = this.imageUrl
341
+ link.download = `${this.imageName}_${Date.now()}.${this.getImageExtension(this.imageUrl)}`
342
+ link.target = '_blank'
343
+ document.body.appendChild(link)
344
+ link.click()
345
+ document.body.removeChild(link)
346
+ } finally {
347
+ this.downloading = false
348
+ }
299
349
  },
300
350
 
301
351
  // 获取图片扩展名
302
- getImageExtension (url) {
303
- const match = url.match(/\.(jpeg|jpg|gif|png|bmp|webp)/)
304
- return match ? match[1] : 'png'
352
+ getImageExtension(url) {
353
+ const match = url.match(/\.(jpeg|jpg|gif|png|bmp|webp)/i)
354
+ return match ? match[1].toLowerCase() : 'png'
305
355
  },
306
356
 
307
357
  // 关闭模态框
308
- handleClose () {
358
+ handleClose() {
309
359
  this.resetImageTransform()
310
360
  this.$emit('close')
311
361
  }
@@ -325,7 +375,7 @@ export default {
325
375
  background: #f5f5f5;
326
376
  position: relative;
327
377
  cursor: grab;
328
- outline: none; /* 移除焦点时的轮廓线 */
378
+ outline: none;
329
379
 
330
380
  &.dragging {
331
381
  cursor: grabbing;
@@ -333,14 +383,55 @@ export default {
333
383
  }
334
384
 
335
385
  .image-wrapper {
336
- display: inline-block;
386
+ display: flex;
387
+ justify-content: center;
388
+ align-items: center;
389
+ width: 100%;
390
+ height: 100%;
337
391
  transition: transform 0.1s ease;
338
392
  will-change: transform;
339
393
  }
340
394
 
341
395
  .preview-image {
342
396
  display: block;
397
+ max-width: calc(100% - 40px);
398
+ max-height: calc(60vh - 40px);
399
+ width: auto;
400
+ height: auto;
401
+ object-fit: contain;
343
402
  transition: transform 0.3s ease;
403
+ user-select: none;
404
+ -webkit-user-drag: none;
405
+ }
406
+
407
+ // 加载中样式
408
+ .loading-wrapper {
409
+ display: flex;
410
+ flex-direction: column;
411
+ justify-content: center;
412
+ align-items: center;
413
+ width: 100%;
414
+ height: 100%;
415
+ }
416
+
417
+ // 加载失败样式
418
+ .error-wrapper {
419
+ display: flex;
420
+ flex-direction: column;
421
+ justify-content: center;
422
+ align-items: center;
423
+ width: 100%;
424
+ height: 100%;
425
+ color: #999;
426
+
427
+ .error-icon {
428
+ font-size: 64px;
429
+ margin-bottom: 16px;
430
+ }
431
+
432
+ p {
433
+ margin-bottom: 8px;
434
+ }
344
435
  }
345
436
 
346
437
  // 顶部工具栏样式
@@ -352,7 +443,7 @@ export default {
352
443
  z-index: 10;
353
444
  display: flex;
354
445
  justify-content: center;
355
- pointer-events: none; // 允许点击穿透到图片
446
+ pointer-events: none;
356
447
 
357
448
  .toolbar-content {
358
449
  display: flex;
@@ -362,7 +453,7 @@ export default {
362
453
  backdrop-filter: blur(5px);
363
454
  border-radius: 20px;
364
455
  padding: 8px 16px;
365
- pointer-events: auto; // 恢复按钮的点击事件
456
+ pointer-events: auto;
366
457
  border: 1px solid rgba(255, 255, 255, 0.1);
367
458
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
368
459