@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/dist/specter-pui-vtk.css +1 -1
- package/dist/specter-pui.es.js +2518 -2438
- package/dist/specter-pui.es.js.map +1 -1
- package/dist/specter-pui.umd.js +1 -1
- package/dist/specter-pui.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/components/assembly/VtkImg.vue +98 -15
- package/src/components/assembly/VtkUpload.vue +194 -195
package/package.json
CHANGED
|
@@ -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
|
-
<
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
54
|
-
|
|
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
|
-
:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
109
|
-
<VIcon>mdi-magnify-minus-outline</VIcon>
|
|
110
|
-
</VBtn>
|
|
111
|
-
<VBtn icon variant="text" @click="zoomPreview(0.
|
|
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
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
const
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
event.
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
target.
|
|
471
|
-
target.addEventListener('
|
|
472
|
-
target.addEventListener('
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
const
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
event.currentTarget?.
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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
|
-
//
|
|
495
|
-
if (
|
|
496
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
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>
|