@yxhl/specter-pui-vtk 1.0.80 → 1.0.82

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": "@yxhl/specter-pui-vtk",
3
- "version": "1.0.80",
3
+ "version": "1.0.82",
4
4
  "description": "雅心互联 Vue 3 + Vuetify3 组件库",
5
5
  "type": "module",
6
6
  "main": "./dist/specter-pui.umd.js",
@@ -37,21 +37,30 @@
37
37
  @click:outside="closePreview"
38
38
  >
39
39
  <VCard class="image-viewer-card">
40
- <v-toolbar dark color="primary">
41
- <VBtn icon dark @click="closePreview">
42
- <VIcon>mdi-close</VIcon>
43
- </VBtn>
44
- <v-toolbar-title>{{ currentImageTitle }}</v-toolbar-title>
45
- <VSpacer></VSpacer>
46
- <v-toolbar-items>
40
+ <v-toolbar dark color="primary">
41
+ <v-toolbar-title>{{ currentImageTitle }}</v-toolbar-title>
42
+ <VSpacer></VSpacer>
43
+ <v-toolbar-items>
44
+ <VBtn icon dark @click="zoomImage(-0.3)">
45
+ <VIcon>mdi-magnify-minus-outline</VIcon>
46
+ </VBtn>
47
+ <VBtn icon dark @click="zoomImage(0.3)">
48
+ <VIcon>mdi-magnify-plus-outline</VIcon>
49
+ </VBtn>
47
50
  <VBtn icon dark @click="rotateImage(-90)">
48
51
  <VIcon>mdi-rotate-left</VIcon>
49
52
  </VBtn>
50
53
  <VBtn icon dark @click="rotateImage(90)">
51
54
  <VIcon>mdi-rotate-right</VIcon>
52
55
  </VBtn>
53
- </v-toolbar-items>
54
- </v-toolbar>
56
+ <VBtn icon dark @click="resetImageTransform">
57
+ <VIcon>mdi-refresh</VIcon>
58
+ </VBtn>
59
+ <VBtn icon dark @click="closePreview">
60
+ <VIcon>mdi-close</VIcon>
61
+ </VBtn>
62
+ </v-toolbar-items>
63
+ </v-toolbar>
55
64
 
56
65
  <div class="image-viewer-content">
57
66
  <VBtn
@@ -63,11 +72,14 @@
63
72
  <VIcon>mdi-chevron-left</VIcon>
64
73
  </VBtn>
65
74
 
66
- <div class="image-container">
75
+ <div class="image-container" @wheel.prevent="handleImageWheel">
67
76
  <v-img
68
77
  :src="currentImageUrl"
69
78
  class="viewer-image"
70
- :style="{ transform: `rotate(${imageRotation}deg)` }"
79
+ :class="{ 'viewer-image--draggable': imageScale > 1 }"
80
+ :style="viewerImageStyle"
81
+ draggable="false"
82
+ @pointerdown="startImageDrag"
71
83
  />
72
84
  </div>
73
85
 
@@ -150,6 +162,11 @@ const props = defineProps({
150
162
  const showPreview = ref(false);
151
163
  // 图片旋转角度
152
164
  const imageRotation = ref(0);
165
+ // 图片缩放和拖动位置
166
+ const imageScale = ref(1);
167
+ const imageTranslateX = ref(0);
168
+ const imageTranslateY = ref(0);
169
+ const imageDragState = ref(null);
153
170
  // 当前图片索引(预览模式下使用)
154
171
  const currentIndex = ref(0);
155
172
 
@@ -225,11 +242,23 @@ const hasNext = computed(() => {
225
242
  return props.imageList.length > 1 && currentIndex.value < props.imageList.length - 1;
226
243
  });
227
244
 
245
+ const viewerImageStyle = computed(() => ({
246
+ transform: `translate(${imageTranslateX.value}px, ${imageTranslateY.value}px) scale(${imageScale.value}) rotate(${imageRotation.value}deg)`,
247
+ }));
248
+
249
+ const resetImageTransform = () => {
250
+ imageRotation.value = 0;
251
+ imageScale.value = 1;
252
+ imageTranslateX.value = 0;
253
+ imageTranslateY.value = 0;
254
+ imageDragState.value = null;
255
+ };
256
+
228
257
  // 监听预览状态变化
229
258
  watch(showPreview, (newVal) => {
230
259
  if (newVal) {
231
260
  // 打开预览时重置旋转角度和当前索引
232
- imageRotation.value = 0;
261
+ resetImageTransform();
233
262
  currentIndex.value = props.index;
234
263
  }
235
264
  });
@@ -250,7 +279,7 @@ const closePreview = () => {
250
279
  const prevImage = () => {
251
280
  if (hasPrev.value) {
252
281
  currentIndex.value--;
253
- imageRotation.value = 0; // 切换图片时重置旋转
282
+ resetImageTransform();
254
283
  }
255
284
  };
256
285
 
@@ -258,7 +287,7 @@ const prevImage = () => {
258
287
  const nextImage = () => {
259
288
  if (hasNext.value) {
260
289
  currentIndex.value++;
261
- imageRotation.value = 0; // 切换图片时重置旋转
290
+ resetImageTransform();
262
291
  }
263
292
  };
264
293
 
@@ -266,6 +295,49 @@ const nextImage = () => {
266
295
  const rotateImage = (degrees) => {
267
296
  imageRotation.value += degrees;
268
297
  };
298
+
299
+ const zoomImage = (step) => {
300
+ const nextScale = Number((imageScale.value + step).toFixed(2));
301
+ imageScale.value = Math.min(3, Math.max(0.2, nextScale));
302
+ if (imageScale.value <= 1) {
303
+ imageTranslateX.value = 0;
304
+ imageTranslateY.value = 0;
305
+ }
306
+ };
307
+
308
+ const handleImageWheel = (event) => {
309
+ zoomImage(event.deltaY < 0 ? 0.2 : -0.2);
310
+ };
311
+
312
+ const startImageDrag = (event) => {
313
+ if (imageScale.value <= 1) return;
314
+ event.preventDefault();
315
+ const target = event.currentTarget;
316
+ imageDragState.value = {
317
+ pointerId: event.pointerId,
318
+ startX: event.clientX,
319
+ startY: event.clientY,
320
+ translateX: imageTranslateX.value,
321
+ translateY: imageTranslateY.value,
322
+ };
323
+ target.setPointerCapture?.(event.pointerId);
324
+ target.addEventListener('pointermove', moveImageDrag);
325
+ target.addEventListener('pointerup', endImageDrag, { once: true });
326
+ target.addEventListener('pointercancel', endImageDrag, { once: true });
327
+ };
328
+
329
+ const moveImageDrag = (event) => {
330
+ const state = imageDragState.value;
331
+ if (!state || state.pointerId !== event.pointerId) return;
332
+ imageTranslateX.value = state.translateX + event.clientX - state.startX;
333
+ imageTranslateY.value = state.translateY + event.clientY - state.startY;
334
+ };
335
+
336
+ const endImageDrag = (event) => {
337
+ event.currentTarget?.releasePointerCapture?.(event.pointerId);
338
+ event.currentTarget?.removeEventListener('pointermove', moveImageDrag);
339
+ imageDragState.value = null;
340
+ };
269
341
  </script>
270
342
 
271
343
  <style lang="scss" scoped>
@@ -330,7 +402,18 @@ const rotateImage = (degrees) => {
330
402
  .viewer-image {
331
403
  max-width: 90%;
332
404
  max-height: 80vh;
405
+ transform-origin: center center;
333
406
  transition: transform 0.3s ease;
407
+ user-select: none;
408
+ touch-action: none;
409
+ }
410
+
411
+ .viewer-image--draggable {
412
+ cursor: grab;
413
+ }
414
+
415
+ .viewer-image--draggable:active {
416
+ cursor: grabbing;
334
417
  }
335
418
 
336
419
  .nav-button {
@@ -369,4 +452,4 @@ const rotateImage = (degrees) => {
369
452
  :deep(.v-img__placeholder) {
370
453
  filter: blur(0px);
371
454
  }
372
- </style>
455
+ </style>
@@ -99,44 +99,44 @@
99
99
  </div>
100
100
 
101
101
  <!-- 预览 Dialog -->
102
- <VDialog v-model="previewVisible" fullscreen scrollable @click:outside="closePreview">
103
- <VCard class="vtk-upload__preview-card">
104
- <v-toolbar color="primary" density="comfortable">
105
- <v-toolbar-title class="text-truncate">{{ previewFile?.name }}</v-toolbar-title>
106
- <VSpacer />
107
- <template v-if="isImage(previewFile)">
108
- <VBtn icon variant="text" @click="zoomPreview(-0.1)">
109
- <VIcon>mdi-magnify-minus-outline</VIcon>
110
- </VBtn>
111
- <VBtn icon variant="text" @click="zoomPreview(0.1)">
112
- <VIcon>mdi-magnify-plus-outline</VIcon>
113
- </VBtn>
114
- <VBtn icon variant="text" @click="rotatePreview(-90)">
115
- <VIcon>mdi-rotate-left</VIcon>
116
- </VBtn>
117
- <VBtn icon variant="text" @click="rotatePreview(90)">
118
- <VIcon>mdi-rotate-right</VIcon>
119
- </VBtn>
120
- <VBtn icon variant="text" @click="resetPreviewTransform">
121
- <VIcon>mdi-refresh</VIcon>
122
- </VBtn>
123
- </template>
124
- <VBtn icon variant="text" @click="closePreview">
125
- <VIcon>mdi-close</VIcon>
126
- </VBtn>
127
- </v-toolbar>
128
- <div class="vtk-upload__preview-body">
129
- <div v-if="isImage(previewFile)" class="vtk-upload__preview-image-wrap">
130
- <img
131
- class="vtk-upload__preview-image"
132
- :src="previewFile?._previewSrc"
133
- :alt="previewFile?.name"
134
- :style="previewImageStyle"
135
- draggable="false"
136
- @pointerdown="startPreviewDrag"
137
- />
138
- </div>
139
- <div v-else class="vtk-upload__preview-empty">
102
+ <VDialog v-model="previewVisible" fullscreen scrollable @click:outside="closePreview">
103
+ <VCard class="vtk-upload__preview-card">
104
+ <v-toolbar color="primary" density="comfortable">
105
+ <v-toolbar-title class="text-truncate">{{ previewFile?.name }}</v-toolbar-title>
106
+ <VSpacer />
107
+ <template v-if="isImage(previewFile)">
108
+ <VBtn icon variant="text" @click="zoomPreview(-0.3)">
109
+ <VIcon>mdi-magnify-minus-outline</VIcon>
110
+ </VBtn>
111
+ <VBtn icon variant="text" @click="zoomPreview(0.3)">
112
+ <VIcon>mdi-magnify-plus-outline</VIcon>
113
+ </VBtn>
114
+ <VBtn icon variant="text" @click="rotatePreview(-90)">
115
+ <VIcon>mdi-rotate-left</VIcon>
116
+ </VBtn>
117
+ <VBtn icon variant="text" @click="rotatePreview(90)">
118
+ <VIcon>mdi-rotate-right</VIcon>
119
+ </VBtn>
120
+ <VBtn icon variant="text" @click="resetPreviewTransform">
121
+ <VIcon>mdi-refresh</VIcon>
122
+ </VBtn>
123
+ </template>
124
+ <VBtn icon variant="text" @click="closePreview">
125
+ <VIcon>mdi-close</VIcon>
126
+ </VBtn>
127
+ </v-toolbar>
128
+ <div class="vtk-upload__preview-body">
129
+ <div v-if="isImage(previewFile)" class="vtk-upload__preview-image-wrap">
130
+ <img
131
+ class="vtk-upload__preview-image"
132
+ :src="previewFile?._previewSrc"
133
+ :alt="previewFile?.name"
134
+ :style="previewImageStyle"
135
+ draggable="false"
136
+ @pointerdown="startPreviewDrag"
137
+ />
138
+ </div>
139
+ <div v-else class="vtk-upload__preview-empty">
140
140
  <VIcon size="64">{{ isExcel(previewFile) ? 'mdi-microsoft-excel' : 'mdi-file-outline' }}</VIcon>
141
141
  <div class="mt-2">{{ previewFile?.name }}</div>
142
142
  <div class="text-caption mt-1">This file cannot be previewed online.</div>
@@ -148,7 +148,7 @@
148
148
  </template>
149
149
 
150
150
  <script setup>
151
- import { computed, ref, watch } from 'vue';
151
+ import { computed, ref, watch } from 'vue';
152
152
  import Request from '../../commons/request.js';
153
153
 
154
154
  defineOptions({
@@ -253,14 +253,14 @@ const emit = defineEmits([
253
253
 
254
254
  /* -------------------- 内部状态 -------------------- */
255
255
  const inputRef = ref(null);
256
- const isDragover = ref(false);
257
- const previewVisible = ref(false);
258
- const previewFile = ref(null);
259
- const previewScale = ref(1);
260
- const previewRotation = ref(0);
261
- const previewTranslateX = ref(0);
262
- const previewTranslateY = ref(0);
263
- const previewDragState = ref(null);
256
+ const isDragover = ref(false);
257
+ const previewVisible = ref(false);
258
+ const previewFile = ref(null);
259
+ const previewScale = ref(1);
260
+ const previewRotation = ref(0);
261
+ const previewTranslateX = ref(0);
262
+ const previewTranslateY = ref(0);
263
+ const previewDragState = ref(null);
264
264
 
265
265
  // 内部维护文件列表
266
266
  const fileList = ref([...(props.modelValue || [])]);
@@ -283,15 +283,15 @@ const isImage = (file) => {
283
283
  return /image\//.test(file.type) || /\.(png|jpg|jpeg|gif|bmp|webp|svg)$/i.test(file.name || '');
284
284
  };
285
285
 
286
- const fileIcon = (file) => {
287
- if (file.status === 'error') return 'mdi-file-alert-outline';
288
- if (isImage(file)) return 'mdi-file-image-outline';
289
- return 'mdi-file-outline';
290
- };
291
-
292
- const previewImageStyle = computed(() => ({
293
- transform: `translate(${previewTranslateX.value}px, ${previewTranslateY.value}px) scale(${previewScale.value}) rotate(${previewRotation.value}deg)`,
294
- }));
286
+ const fileIcon = (file) => {
287
+ if (file.status === 'error') return 'mdi-file-alert-outline';
288
+ if (isImage(file)) return 'mdi-file-image-outline';
289
+ return 'mdi-file-outline';
290
+ };
291
+
292
+ const previewImageStyle = computed(() => ({
293
+ transform: `translate(${previewTranslateX.value}px, ${previewTranslateY.value}px) scale(${previewScale.value}) rotate(${previewRotation.value}deg)`,
294
+ }));
295
295
 
296
296
  /* -------------------- 触发 input -------------------- */
297
297
  const triggerInput = () => {
@@ -433,88 +433,88 @@ const handleRemove = (file) => {
433
433
  };
434
434
 
435
435
  /* -------------------- 预览 -------------------- */
436
- const isPdf = (file) => /\.pdf$/i.test(file?.name || '') || file?.type === 'application/pdf';
437
- const isExcel = (file) => /\.(xlsx|xls)$/i.test(file?.name || '');
438
-
439
- const resetPreviewTransform = () => {
440
- previewScale.value = 1;
441
- previewRotation.value = 0;
442
- previewTranslateX.value = 0;
443
- previewTranslateY.value = 0;
444
- };
445
-
446
- const closePreview = () => {
447
- previewVisible.value = false;
448
- };
449
-
450
- const rotatePreview = (degrees) => {
451
- previewRotation.value += degrees;
452
- };
453
-
454
- const zoomPreview = (step) => {
455
- const nextScale = Number((previewScale.value + step).toFixed(2));
456
- previewScale.value = Math.min(3, Math.max(0.2, nextScale));
457
- };
458
-
459
- const startPreviewDrag = (event) => {
460
- if (previewScale.value <= 1) return;
461
- event.preventDefault();
462
- const target = event.currentTarget;
463
- previewDragState.value = {
464
- pointerId: event.pointerId,
465
- startX: event.clientX,
466
- startY: event.clientY,
467
- translateX: previewTranslateX.value,
468
- translateY: previewTranslateY.value,
469
- };
470
- target.setPointerCapture?.(event.pointerId);
471
- target.addEventListener('pointermove', movePreviewDrag);
472
- target.addEventListener('pointerup', endPreviewDrag, { once: true });
473
- target.addEventListener('pointercancel', endPreviewDrag, { once: true });
474
- };
475
-
476
- const movePreviewDrag = (event) => {
477
- const state = previewDragState.value;
478
- if (!state || state.pointerId !== event.pointerId) return;
479
- previewTranslateX.value = state.translateX + event.clientX - state.startX;
480
- previewTranslateY.value = state.translateY + event.clientY - state.startY;
481
- };
482
-
483
- const endPreviewDrag = (event) => {
484
- event.currentTarget?.releasePointerCapture?.(event.pointerId);
485
- event.currentTarget?.removeEventListener('pointermove', movePreviewDrag);
486
- previewDragState.value = null;
487
- };
488
-
489
- const handlePreview = (file) => {
490
- emit('preview', file);
436
+ const isExcel = (file) => /\.(xlsx|xls)$/i.test(file?.name || '');
437
+
438
+ const resetPreviewTransform = () => {
439
+ previewScale.value = 1;
440
+ previewRotation.value = 0;
441
+ previewTranslateX.value = 0;
442
+ previewTranslateY.value = 0;
443
+ };
444
+
445
+ const closePreview = () => {
446
+ previewVisible.value = false;
447
+ };
448
+
449
+ const rotatePreview = (degrees) => {
450
+ previewRotation.value += degrees;
451
+ };
452
+
453
+ const zoomPreview = (step) => {
454
+ const nextScale = Number((previewScale.value + step).toFixed(2));
455
+ previewScale.value = Math.min(3, Math.max(0.2, nextScale));
456
+ };
457
+
458
+ const startPreviewDrag = (event) => {
459
+ if (previewScale.value <= 1) return;
460
+ event.preventDefault();
461
+ const target = event.currentTarget;
462
+ previewDragState.value = {
463
+ pointerId: event.pointerId,
464
+ startX: event.clientX,
465
+ startY: event.clientY,
466
+ translateX: previewTranslateX.value,
467
+ translateY: previewTranslateY.value,
468
+ };
469
+ target.setPointerCapture?.(event.pointerId);
470
+ target.addEventListener('pointermove', movePreviewDrag);
471
+ target.addEventListener('pointerup', endPreviewDrag, { once: true });
472
+ target.addEventListener('pointercancel', endPreviewDrag, { once: true });
473
+ };
474
+
475
+ const movePreviewDrag = (event) => {
476
+ const state = previewDragState.value;
477
+ if (!state || state.pointerId !== event.pointerId) return;
478
+ previewTranslateX.value = state.translateX + event.clientX - state.startX;
479
+ previewTranslateY.value = state.translateY + event.clientY - state.startY;
480
+ };
481
+
482
+ const endPreviewDrag = (event) => {
483
+ event.currentTarget?.releasePointerCapture?.(event.pointerId);
484
+ event.currentTarget?.removeEventListener('pointermove', movePreviewDrag);
485
+ previewDragState.value = null;
486
+ };
487
+
488
+ const openFileInNewTab = (file, fileUrl) => {
489
+ if (fileUrl) {
490
+ window.open(fileUrl, '_blank');
491
+ return;
492
+ }
493
+
494
+ if (file.raw instanceof File || file.raw instanceof Blob) {
495
+ const blobUrl = URL.createObjectURL(file.raw);
496
+ const win = window.open(blobUrl, '_blank');
497
+ win?.addEventListener('unload', () => URL.revokeObjectURL(blobUrl));
498
+ }
499
+ };
500
+
501
+ const handlePreview = (file) => {
502
+ emit('preview', file);
491
503
 
492
504
  const fileUrl = file.url || file.preview;
493
505
 
494
- // PDF:新标签页直接打开(浏览器原生支持)
495
- if (isPdf(file)) {
496
- if (fileUrl) {
497
- window.open(fileUrl, '_blank');
498
- } else if (file.raw instanceof File || file.raw instanceof Blob) {
499
- // 本地未上传文件,用 Blob URL 打开
500
- const blobUrl = URL.createObjectURL(file.raw);
501
- const win = window.open(blobUrl, '_blank');
502
- win?.addEventListener('unload', () => URL.revokeObjectURL(blobUrl));
503
- } else {
504
- // 无可用地址也无本地文件,降级到 Dialog
505
- previewFile.value = { ...file, _previewSrc: fileUrl };
506
- resetPreviewTransform();
507
- previewVisible.value = true;
508
- }
506
+ // Non-image files open in a new tab.
507
+ if (!isImage(file) && !isExcel(file)) {
508
+ openFileInNewTab(file, fileUrl);
509
509
  return;
510
510
  }
511
511
 
512
512
  // Excel:Office Online 查看器(需要文件有公网 URL)
513
513
  if (isExcel(file)) {
514
- if (fileUrl && /^https?:\/\//.test(fileUrl)) {
515
- window.open(`https://view.officeapps.live.com/op/view.aspx?src=${encodeURIComponent(fileUrl)}`, '_blank');
516
- } else {
517
- // 无公网地址,降级为 Dialog 显示提示
514
+ if (fileUrl && /^https?:\/\//.test(fileUrl)) {
515
+ window.open(`https://view.officeapps.live.com/op/view.aspx?src=${encodeURIComponent(fileUrl)}`, '_blank');
516
+ } else {
517
+ // Excel without a public URL falls back to the dialog hint.
518
518
  previewFile.value = { ...file, _previewSrc: fileUrl };
519
519
  resetPreviewTransform();
520
520
  previewVisible.value = true;
@@ -522,19 +522,18 @@ const handlePreview = (file) => {
522
522
  return;
523
523
  }
524
524
 
525
- // 图片及其他:Dialog 内嵌预览
526
- // 统一挂载可用的预览地址,供 Dialog 使用
527
- previewFile.value = {
528
- ...file,
529
- _previewSrc: fileUrl,
530
- };
531
- resetPreviewTransform();
532
- previewVisible.value = true;
533
- };
534
-
535
- watch(previewVisible, (visible) => {
536
- if (!visible) resetPreviewTransform();
537
- });
525
+ // Images preview inside the dialog.
526
+ previewFile.value = {
527
+ ...file,
528
+ _previewSrc: fileUrl,
529
+ };
530
+ resetPreviewTransform();
531
+ previewVisible.value = true;
532
+ };
533
+
534
+ watch(previewVisible, (visible) => {
535
+ if (!visible) resetPreviewTransform();
536
+ });
538
537
 
539
538
  /* -------------------- 对外暴露方法 -------------------- */
540
539
  /** 手动触发未上传文件的上传 */
@@ -688,55 +687,55 @@ defineExpose({ submit, clearFiles, remove });
688
687
  }
689
688
 
690
689
  /* ---- 隐藏 input ---- */
691
- .vtk-upload__input {
692
- display: none;
693
- }
694
-
695
- /* ---- preview ---- */
696
- .vtk-upload__preview-card {
697
- height: 100vh;
698
- background: #111;
699
- }
700
-
701
- .vtk-upload__preview-body {
702
- height: calc(100vh - 64px);
703
- overflow: auto;
704
- background: #111;
705
- }
706
-
707
- .vtk-upload__preview-image-wrap {
708
- min-width: 100%;
709
- min-height: 100%;
710
- padding: 32px;
711
- display: flex;
712
- align-items: center;
713
- justify-content: center;
714
- }
715
-
716
- .vtk-upload__preview-image {
717
- display: block;
718
- max-width: 90vw;
719
- max-height: calc(100vh - 128px);
720
- object-fit: contain;
721
- transform-origin: center center;
722
- transition: transform 0.2s ease;
723
- user-select: none;
724
- cursor: grab;
725
- touch-action: none;
726
-
727
- &:active {
728
- cursor: grabbing;
729
- }
730
- }
731
-
732
- .vtk-upload__preview-empty {
733
- min-height: 100%;
734
- display: flex;
735
- flex-direction: column;
736
- align-items: center;
737
- justify-content: center;
738
- padding: 32px;
739
- color: rgba(255, 255, 255, 0.72);
740
- text-align: center;
741
- }
742
- </style>
690
+ .vtk-upload__input {
691
+ display: none;
692
+ }
693
+
694
+ /* ---- preview ---- */
695
+ .vtk-upload__preview-card {
696
+ height: 100vh;
697
+ background: #111;
698
+ }
699
+
700
+ .vtk-upload__preview-body {
701
+ height: calc(100vh - 64px);
702
+ overflow: auto;
703
+ background: #111;
704
+ }
705
+
706
+ .vtk-upload__preview-image-wrap {
707
+ min-width: 100%;
708
+ min-height: 100%;
709
+ padding: 32px;
710
+ display: flex;
711
+ align-items: center;
712
+ justify-content: center;
713
+ }
714
+
715
+ .vtk-upload__preview-image {
716
+ display: block;
717
+ max-width: 90vw;
718
+ max-height: calc(100vh - 128px);
719
+ object-fit: contain;
720
+ transform-origin: center center;
721
+ transition: transform 0.2s ease;
722
+ user-select: none;
723
+ cursor: grab;
724
+ touch-action: none;
725
+
726
+ &:active {
727
+ cursor: grabbing;
728
+ }
729
+ }
730
+
731
+ .vtk-upload__preview-empty {
732
+ min-height: 100%;
733
+ display: flex;
734
+ flex-direction: column;
735
+ align-items: center;
736
+ justify-content: center;
737
+ padding: 32px;
738
+ color: rgba(255, 255, 255, 0.72);
739
+ text-align: center;
740
+ }
741
+ </style>