@yxhl/specter-pui-vtk 1.0.85 → 1.0.86
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 +2488 -2403
- 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 +13 -13
- package/src/components/assembly/VtkUpload.vue +261 -158
package/package.json
CHANGED
|
@@ -37,10 +37,10 @@
|
|
|
37
37
|
@click:outside="closePreview"
|
|
38
38
|
>
|
|
39
39
|
<VCard class="image-viewer-card">
|
|
40
|
-
<v-toolbar dark color="primary">
|
|
41
|
-
<v-toolbar-title>{{ currentImageTitle }}</v-toolbar-title>
|
|
42
|
-
<VSpacer></VSpacer>
|
|
43
|
-
<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
44
|
<VBtn icon dark @click="zoomImage(-0.3)">
|
|
45
45
|
<VIcon>mdi-magnify-minus-outline</VIcon>
|
|
46
46
|
</VBtn>
|
|
@@ -53,14 +53,14 @@
|
|
|
53
53
|
<VBtn icon dark @click="rotateImage(90)">
|
|
54
54
|
<VIcon>mdi-rotate-right</VIcon>
|
|
55
55
|
</VBtn>
|
|
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>
|
|
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>
|
|
64
64
|
|
|
65
65
|
<div class="image-viewer-content">
|
|
66
66
|
<VBtn
|
|
@@ -379,7 +379,7 @@ const endImageDrag = (event) => {
|
|
|
379
379
|
|
|
380
380
|
/* 图片查看器样式 */
|
|
381
381
|
.image-viewer-card {
|
|
382
|
-
background-color: rgba(0, 0, 0, 0.
|
|
382
|
+
background-color: rgba(0, 0, 0, 0.3);
|
|
383
383
|
color: white;
|
|
384
384
|
}
|
|
385
385
|
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
<div class="vtk-upload">
|
|
3
3
|
<!-- 拖拽 / 点击上传区域 -->
|
|
4
4
|
<div
|
|
5
|
-
v-if="listType !== 'picture-card' && !detail"
|
|
6
|
-
:class="['vtk-upload__dragger', { 'is-dragover': isDragover, 'is-disabled': disabled }]"
|
|
7
|
-
title="点击上传"
|
|
8
|
-
@click="!disabled && triggerInput()"
|
|
5
|
+
v-if="listType !== 'picture-card' && !detail"
|
|
6
|
+
:class="['vtk-upload__dragger', { 'is-dragover': isDragover, 'is-disabled': disabled }]"
|
|
7
|
+
title="点击上传"
|
|
8
|
+
@click="!disabled && triggerInput()"
|
|
9
9
|
@dragover.prevent="onDragover"
|
|
10
10
|
@dragleave.prevent="isDragover = false"
|
|
11
11
|
@drop.prevent="onDrop"
|
|
@@ -28,12 +28,12 @@
|
|
|
28
28
|
>
|
|
29
29
|
<v-img :src="file.url || file.preview" cover class="fill-height" />
|
|
30
30
|
<div class="vtk-upload__picture-card-mask">
|
|
31
|
-
<VBtn icon size="x-small" variant="text" color="white" title="查看" @click.stop="handlePreview(file)">
|
|
32
|
-
<VIcon>mdi-eye</VIcon>
|
|
33
|
-
</VBtn>
|
|
34
|
-
<VBtn v-if="!disabled && !detail" icon size="x-small" variant="text" color="white" title="删除" @click.stop="handleRemove(file)">
|
|
35
|
-
<VIcon>mdi-delete</VIcon>
|
|
36
|
-
</VBtn>
|
|
31
|
+
<VBtn icon size="x-small" variant="text" color="white" title="查看" @click.stop="handlePreview(file)">
|
|
32
|
+
<VIcon>mdi-eye</VIcon>
|
|
33
|
+
</VBtn>
|
|
34
|
+
<VBtn v-if="!disabled && !detail" icon size="x-small" variant="text" color="white" title="删除" @click.stop="handleRemove(file)">
|
|
35
|
+
<VIcon>mdi-delete</VIcon>
|
|
36
|
+
</VBtn>
|
|
37
37
|
</div>
|
|
38
38
|
<!-- 上传进度 -->
|
|
39
39
|
<div v-if="file.status === 'uploading'" class="vtk-upload__picture-card-progress">
|
|
@@ -43,10 +43,10 @@
|
|
|
43
43
|
|
|
44
44
|
<!-- 添加按钮 -->
|
|
45
45
|
<div
|
|
46
|
-
v-if="!disabled && !detail && (limit === 0 || fileList.length < limit)"
|
|
47
|
-
:class="['vtk-upload__picture-card-add', { 'is-dragover': isDragover }]"
|
|
48
|
-
title="添加文件"
|
|
49
|
-
@click="triggerInput()"
|
|
46
|
+
v-if="!disabled && !detail && (limit === 0 || fileList.length < limit)"
|
|
47
|
+
:class="['vtk-upload__picture-card-add', { 'is-dragover': isDragover }]"
|
|
48
|
+
title="添加文件"
|
|
49
|
+
@click="triggerInput()"
|
|
50
50
|
@dragover.prevent="onDragover"
|
|
51
51
|
@dragleave.prevent="isDragover = false"
|
|
52
52
|
@drop.prevent="onDrop"
|
|
@@ -79,14 +79,14 @@
|
|
|
79
79
|
<span class="vtk-upload__list-item-name flex-grow-1 text-truncate" @click="handlePreview(file)" :title="file.name">{{ file.name }}</span>
|
|
80
80
|
<span v-if="file.status === 'uploading'" class="text-caption text-grey ml-2">{{ file.percentage }}%</span>
|
|
81
81
|
<VBtn
|
|
82
|
-
v-if="!disabled && !detail"
|
|
82
|
+
v-if="!disabled && !detail"
|
|
83
83
|
icon
|
|
84
84
|
size="x-small"
|
|
85
|
-
variant="text"
|
|
86
|
-
color="grey"
|
|
87
|
-
class="ml-1"
|
|
88
|
-
title="删除"
|
|
89
|
-
@click="handleRemove(file)"
|
|
85
|
+
variant="text"
|
|
86
|
+
color="grey"
|
|
87
|
+
class="ml-1"
|
|
88
|
+
title="删除"
|
|
89
|
+
@click="handleRemove(file)"
|
|
90
90
|
>
|
|
91
91
|
<VIcon size="16">mdi-close</VIcon>
|
|
92
92
|
</VBtn>
|
|
@@ -105,33 +105,57 @@
|
|
|
105
105
|
<VDialog v-model="previewVisible" fullscreen scrollable @click:outside="closePreview">
|
|
106
106
|
<VCard class="vtk-upload__preview-card">
|
|
107
107
|
<v-toolbar color="primary" density="comfortable">
|
|
108
|
-
<v-toolbar-title class="text-truncate">{{ previewFile?.name }}</v-toolbar-title>
|
|
109
|
-
<VSpacer />
|
|
110
|
-
<VBtn icon variant="text" title="新窗口打开" @click="openCurrentPreview">
|
|
111
|
-
<VIcon>mdi-open-in-new</VIcon>
|
|
112
|
-
</VBtn>
|
|
113
|
-
<template v-if="isImage(previewFile)">
|
|
114
|
-
<VBtn icon variant="text" title="
|
|
115
|
-
<VIcon>mdi-
|
|
116
|
-
</VBtn>
|
|
117
|
-
<VBtn icon variant="text" title="
|
|
118
|
-
<VIcon>mdi-
|
|
119
|
-
</VBtn>
|
|
120
|
-
<VBtn icon variant="text" title="
|
|
121
|
-
<VIcon>mdi-
|
|
122
|
-
</VBtn>
|
|
123
|
-
<VBtn icon variant="text" title="
|
|
124
|
-
<VIcon>mdi-
|
|
125
|
-
</VBtn>
|
|
126
|
-
<VBtn icon variant="text" title="
|
|
127
|
-
<VIcon>mdi-
|
|
128
|
-
</VBtn>
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
108
|
+
<v-toolbar-title class="text-truncate">{{ previewFile?.name }}</v-toolbar-title>
|
|
109
|
+
<VSpacer />
|
|
110
|
+
<VBtn icon variant="text" title="新窗口打开" @click="openCurrentPreview">
|
|
111
|
+
<VIcon>mdi-open-in-new</VIcon>
|
|
112
|
+
</VBtn>
|
|
113
|
+
<template v-if="isImage(previewFile)">
|
|
114
|
+
<VBtn icon variant="text" :disabled="!hasPreviewPrev" title="上一张" @click="switchPreviewImage(-1)">
|
|
115
|
+
<VIcon>mdi-chevron-left</VIcon>
|
|
116
|
+
</VBtn>
|
|
117
|
+
<VBtn icon variant="text" :disabled="!hasPreviewNext" title="下一张" @click="switchPreviewImage(1)">
|
|
118
|
+
<VIcon>mdi-chevron-right</VIcon>
|
|
119
|
+
</VBtn>
|
|
120
|
+
<VBtn icon variant="text" title="缩小" @click="zoomPreview(-0.3)">
|
|
121
|
+
<VIcon>mdi-magnify-minus-outline</VIcon>
|
|
122
|
+
</VBtn>
|
|
123
|
+
<VBtn icon variant="text" title="放大" @click="zoomPreview(0.3)">
|
|
124
|
+
<VIcon>mdi-magnify-plus-outline</VIcon>
|
|
125
|
+
</VBtn>
|
|
126
|
+
<VBtn icon variant="text" title="向左旋转" @click="rotatePreview(-90)">
|
|
127
|
+
<VIcon>mdi-rotate-left</VIcon>
|
|
128
|
+
</VBtn>
|
|
129
|
+
<VBtn icon variant="text" title="向右旋转" @click="rotatePreview(90)">
|
|
130
|
+
<VIcon>mdi-rotate-right</VIcon>
|
|
131
|
+
</VBtn>
|
|
132
|
+
<VBtn icon variant="text" title="重置" @click="resetPreviewTransform">
|
|
133
|
+
<VIcon>mdi-refresh</VIcon>
|
|
134
|
+
</VBtn>
|
|
135
|
+
</template>
|
|
136
|
+
<VBtn icon variant="text" title="关闭" @click="closePreview">
|
|
137
|
+
<VIcon>mdi-close</VIcon>
|
|
138
|
+
</VBtn>
|
|
133
139
|
</v-toolbar>
|
|
134
140
|
<div class="vtk-upload__preview-body">
|
|
141
|
+
<VBtn
|
|
142
|
+
v-if="isImage(previewFile) && hasPreviewPrev"
|
|
143
|
+
icon
|
|
144
|
+
class="vtk-upload__preview-nav vtk-upload__preview-nav--prev"
|
|
145
|
+
title="上一张"
|
|
146
|
+
@click.stop="switchPreviewImage(-1)"
|
|
147
|
+
>
|
|
148
|
+
<VIcon size="36">mdi-chevron-left</VIcon>
|
|
149
|
+
</VBtn>
|
|
150
|
+
<VBtn
|
|
151
|
+
v-if="isImage(previewFile) && hasPreviewNext"
|
|
152
|
+
icon
|
|
153
|
+
class="vtk-upload__preview-nav vtk-upload__preview-nav--next"
|
|
154
|
+
title="下一张"
|
|
155
|
+
@click.stop="switchPreviewImage(1)"
|
|
156
|
+
>
|
|
157
|
+
<VIcon size="36">mdi-chevron-right</VIcon>
|
|
158
|
+
</VBtn>
|
|
135
159
|
<div v-if="isImage(previewFile)" class="vtk-upload__preview-image-wrap">
|
|
136
160
|
<img
|
|
137
161
|
class="vtk-upload__preview-image"
|
|
@@ -154,7 +178,7 @@
|
|
|
154
178
|
</template>
|
|
155
179
|
|
|
156
180
|
<script setup>
|
|
157
|
-
import { computed, nextTick, ref, watch } from 'vue';
|
|
181
|
+
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue';
|
|
158
182
|
import Request from '../../commons/request.js';
|
|
159
183
|
|
|
160
184
|
defineOptions({
|
|
@@ -210,15 +234,15 @@ const props = defineProps({
|
|
|
210
234
|
default: true,
|
|
211
235
|
},
|
|
212
236
|
/** 是否禁用 */
|
|
213
|
-
disabled: {
|
|
214
|
-
type: Boolean,
|
|
215
|
-
default: false,
|
|
216
|
-
},
|
|
217
|
-
/** 详情模式:隐藏上传和删除入口,仅展示已上传文件并保留查看 */
|
|
218
|
-
detail: {
|
|
219
|
-
type: Boolean,
|
|
220
|
-
default: false,
|
|
221
|
-
},
|
|
237
|
+
disabled: {
|
|
238
|
+
type: Boolean,
|
|
239
|
+
default: false,
|
|
240
|
+
},
|
|
241
|
+
/** 详情模式:隐藏上传和删除入口,仅展示已上传文件并保留查看 */
|
|
242
|
+
detail: {
|
|
243
|
+
type: Boolean,
|
|
244
|
+
default: false,
|
|
245
|
+
},
|
|
222
246
|
/** 附加请求头 */
|
|
223
247
|
headers: {
|
|
224
248
|
type: Object,
|
|
@@ -273,87 +297,105 @@ const previewTranslateX = ref(0);
|
|
|
273
297
|
const previewTranslateY = ref(0);
|
|
274
298
|
const previewDragState = ref(null);
|
|
275
299
|
|
|
276
|
-
// 内部维护文件列表
|
|
277
|
-
const fileList = ref([]);
|
|
278
|
-
let isSyncingModel = false;
|
|
279
|
-
|
|
280
|
-
watch(
|
|
281
|
-
() => props.modelValue,
|
|
282
|
-
(val) => {
|
|
283
|
-
if (isSyncingModel) {
|
|
284
|
-
isSyncingModel = false;
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// Accept external URL lists or file object lists.
|
|
289
|
-
fileList.value = normalizeFileList(val || []);
|
|
290
|
-
},
|
|
291
|
-
);
|
|
300
|
+
// 内部维护文件列表
|
|
301
|
+
const fileList = ref([]);
|
|
302
|
+
let isSyncingModel = false;
|
|
303
|
+
|
|
304
|
+
watch(
|
|
305
|
+
() => props.modelValue,
|
|
306
|
+
(val) => {
|
|
307
|
+
if (isSyncingModel) {
|
|
308
|
+
isSyncingModel = false;
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Accept external URL lists or file object lists.
|
|
313
|
+
fileList.value = normalizeFileList(val || []);
|
|
314
|
+
},
|
|
315
|
+
);
|
|
292
316
|
|
|
293
317
|
/* -------------------- 工具函数 -------------------- */
|
|
294
|
-
let uidCounter = 0;
|
|
295
|
-
const genUid = () => `vtk-upload-${Date.now()}-${uidCounter++}`;
|
|
296
|
-
|
|
318
|
+
let uidCounter = 0;
|
|
319
|
+
const genUid = () => `vtk-upload-${Date.now()}-${uidCounter++}`;
|
|
320
|
+
|
|
297
321
|
const getFileNameFromUrl = (url) => {
|
|
298
322
|
const cleanUrl = String(url || '').split('?')[0].split('#')[0];
|
|
299
323
|
const name = cleanUrl.substring(cleanUrl.lastIndexOf('/') + 1);
|
|
300
324
|
try {
|
|
301
325
|
return decodeURIComponent(name || cleanUrl || 'file');
|
|
302
|
-
} catch (e) {
|
|
303
|
-
return name || cleanUrl || 'file';
|
|
326
|
+
} catch (e) {
|
|
327
|
+
return name || cleanUrl || 'file';
|
|
304
328
|
}
|
|
305
329
|
};
|
|
306
330
|
|
|
331
|
+
const isImageUrl = (url) => {
|
|
332
|
+
const value = String(url || '');
|
|
333
|
+
const cleanUrl = value.split('?')[0].split('#')[0];
|
|
334
|
+
return /^data:image\//i.test(value) || /\.(png|jpg|jpeg|gif|bmp|webp|svg)$/i.test(cleanUrl);
|
|
335
|
+
};
|
|
336
|
+
|
|
307
337
|
const normalizeFileList = (value) => {
|
|
308
338
|
if (!Array.isArray(value)) return [];
|
|
309
339
|
|
|
310
340
|
return value
|
|
311
|
-
.filter((item) => item)
|
|
312
|
-
.map((item) => {
|
|
313
|
-
if (typeof item === 'string') {
|
|
314
|
-
return {
|
|
341
|
+
.filter((item) => item)
|
|
342
|
+
.map((item) => {
|
|
343
|
+
if (typeof item === 'string') {
|
|
344
|
+
return {
|
|
315
345
|
uid: genUid(),
|
|
316
346
|
name: getFileNameFromUrl(item),
|
|
317
347
|
size: 0,
|
|
318
|
-
type: '',
|
|
348
|
+
type: props.listType === 'picture-card' || isImageUrl(item) ? 'image/*' : '',
|
|
319
349
|
status: 'success',
|
|
320
350
|
percentage: 100,
|
|
321
351
|
raw: null,
|
|
322
|
-
url: item,
|
|
323
|
-
preview: '',
|
|
324
|
-
response: null,
|
|
325
|
-
};
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
return {
|
|
352
|
+
url: item,
|
|
353
|
+
preview: '',
|
|
354
|
+
response: null,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return {
|
|
329
359
|
uid: item.uid || genUid(),
|
|
330
360
|
name: item.name || getFileNameFromUrl(item.url || item.preview),
|
|
331
361
|
size: item.size || 0,
|
|
332
|
-
type: item.type || '',
|
|
362
|
+
type: item.type || (props.listType === 'picture-card' && (item.url || item.preview) ? 'image/*' : ''),
|
|
333
363
|
status: item.status || 'success',
|
|
334
364
|
percentage: item.percentage ?? 100,
|
|
335
365
|
raw: item.raw || null,
|
|
336
|
-
url: item.url || '',
|
|
337
|
-
preview: item.preview || '',
|
|
338
|
-
response: item.response || null,
|
|
339
|
-
...item,
|
|
340
|
-
};
|
|
341
|
-
});
|
|
342
|
-
};
|
|
343
|
-
|
|
344
|
-
fileList.value = normalizeFileList(props.modelValue || []);
|
|
345
|
-
|
|
346
|
-
const isImage = (file) => {
|
|
347
|
-
if (!file) return false;
|
|
348
|
-
return /image\//.test(file.type) || /\.(png|jpg|jpeg|gif|bmp|webp|svg)$/i.test(file.name || '');
|
|
366
|
+
url: item.url || '',
|
|
367
|
+
preview: item.preview || '',
|
|
368
|
+
response: item.response || null,
|
|
369
|
+
...item,
|
|
370
|
+
};
|
|
371
|
+
});
|
|
349
372
|
};
|
|
350
373
|
|
|
374
|
+
fileList.value = normalizeFileList(props.modelValue || []);
|
|
375
|
+
|
|
376
|
+
const isImage = (file) => {
|
|
377
|
+
if (!file) return false;
|
|
378
|
+
return /image\//.test(file.type) || isImageUrl(file.name) || isImageUrl(file.url) || isImageUrl(file.preview);
|
|
379
|
+
};
|
|
380
|
+
|
|
351
381
|
const fileIcon = (file) => {
|
|
352
382
|
if (file.status === 'error') return 'mdi-file-alert-outline';
|
|
353
383
|
if (isImage(file)) return 'mdi-file-image-outline';
|
|
354
384
|
return 'mdi-file-outline';
|
|
355
385
|
};
|
|
356
386
|
|
|
387
|
+
const getPreviewSrc = (file) => file?.url || file?.preview || '';
|
|
388
|
+
|
|
389
|
+
const previewImageList = computed(() => fileList.value.filter((file) => isImage(file) && getPreviewSrc(file)));
|
|
390
|
+
|
|
391
|
+
const previewImageIndex = computed(() => {
|
|
392
|
+
if (!previewFile.value) return -1;
|
|
393
|
+
return previewImageList.value.findIndex((file) => file.uid === previewFile.value.uid);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
const hasPreviewPrev = computed(() => previewImageIndex.value > 0);
|
|
397
|
+
const hasPreviewNext = computed(() => previewImageIndex.value >= 0 && previewImageIndex.value < previewImageList.value.length - 1);
|
|
398
|
+
|
|
357
399
|
const previewImageStyle = computed(() => ({
|
|
358
400
|
transform: `translate(${previewTranslateX.value}px, ${previewTranslateY.value}px) scale(${previewScale.value}) rotate(${previewRotation.value}deg)`,
|
|
359
401
|
}));
|
|
@@ -364,20 +406,20 @@ const triggerInput = () => {
|
|
|
364
406
|
};
|
|
365
407
|
|
|
366
408
|
/* -------------------- 拖拽 -------------------- */
|
|
367
|
-
const onDragover = () => {
|
|
368
|
-
if (!props.disabled && !props.detail) isDragover.value = true;
|
|
369
|
-
};
|
|
370
|
-
|
|
371
|
-
const onDrop = (e) => {
|
|
372
|
-
isDragover.value = false;
|
|
373
|
-
if (props.disabled || props.detail) return;
|
|
374
|
-
processFiles(Array.from(e.dataTransfer.files));
|
|
375
|
-
};
|
|
409
|
+
const onDragover = () => {
|
|
410
|
+
if (!props.disabled && !props.detail) isDragover.value = true;
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
const onDrop = (e) => {
|
|
414
|
+
isDragover.value = false;
|
|
415
|
+
if (props.disabled || props.detail) return;
|
|
416
|
+
processFiles(Array.from(e.dataTransfer.files));
|
|
417
|
+
};
|
|
376
418
|
|
|
377
419
|
/* -------------------- input change -------------------- */
|
|
378
|
-
const onInputChange = (e) => {
|
|
379
|
-
if (props.disabled || props.detail) return;
|
|
380
|
-
processFiles(Array.from(e.target.files));
|
|
420
|
+
const onInputChange = (e) => {
|
|
421
|
+
if (props.disabled || props.detail) return;
|
|
422
|
+
processFiles(Array.from(e.target.files));
|
|
381
423
|
// 清空,允许重复选同一文件
|
|
382
424
|
e.target.value = '';
|
|
383
425
|
};
|
|
@@ -439,16 +481,16 @@ const addFile = (file) => {
|
|
|
439
481
|
emit('change', file, fileList.value);
|
|
440
482
|
};
|
|
441
483
|
|
|
442
|
-
const syncModel = () => {
|
|
443
|
-
const urls = fileList.value
|
|
444
|
-
.filter((f) => f.status === 'success' && f.url)
|
|
445
|
-
.map((f) => String(f.url));
|
|
446
|
-
isSyncingModel = true;
|
|
447
|
-
emit('update:modelValue', urls);
|
|
448
|
-
nextTick(() => {
|
|
449
|
-
isSyncingModel = false;
|
|
450
|
-
});
|
|
451
|
-
};
|
|
484
|
+
const syncModel = () => {
|
|
485
|
+
const urls = fileList.value
|
|
486
|
+
.filter((f) => f.status === 'success' && f.url)
|
|
487
|
+
.map((f) => String(f.url));
|
|
488
|
+
isSyncingModel = true;
|
|
489
|
+
emit('update:modelValue', urls);
|
|
490
|
+
nextTick(() => {
|
|
491
|
+
isSyncingModel = false;
|
|
492
|
+
});
|
|
493
|
+
};
|
|
452
494
|
|
|
453
495
|
/* -------------------- 上传逻辑 -------------------- */
|
|
454
496
|
const uploadFile = (file) => {
|
|
@@ -525,6 +567,39 @@ const zoomPreview = (step) => {
|
|
|
525
567
|
previewScale.value = Math.min(3, Math.max(0.2, nextScale));
|
|
526
568
|
};
|
|
527
569
|
|
|
570
|
+
const setImagePreviewFile = (file) => {
|
|
571
|
+
previewFile.value = {
|
|
572
|
+
...file,
|
|
573
|
+
_previewSrc: getPreviewSrc(file),
|
|
574
|
+
};
|
|
575
|
+
resetPreviewTransform();
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
const switchPreviewImage = (step) => {
|
|
579
|
+
if (!isImage(previewFile.value)) return;
|
|
580
|
+
|
|
581
|
+
const index = previewImageIndex.value;
|
|
582
|
+
const nextFile = previewImageList.value[index + step];
|
|
583
|
+
if (!nextFile) return;
|
|
584
|
+
|
|
585
|
+
emit('preview', nextFile);
|
|
586
|
+
setImagePreviewFile(nextFile);
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
const handlePreviewKeydown = (event) => {
|
|
590
|
+
if (!previewVisible.value || !isImage(previewFile.value)) return;
|
|
591
|
+
|
|
592
|
+
if (event.key === 'ArrowLeft' && hasPreviewPrev.value) {
|
|
593
|
+
event.preventDefault();
|
|
594
|
+
switchPreviewImage(-1);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (event.key === 'ArrowRight' && hasPreviewNext.value) {
|
|
598
|
+
event.preventDefault();
|
|
599
|
+
switchPreviewImage(1);
|
|
600
|
+
}
|
|
601
|
+
};
|
|
602
|
+
|
|
528
603
|
const startPreviewDrag = (event) => {
|
|
529
604
|
if (previewScale.value <= 1) return;
|
|
530
605
|
event.preventDefault();
|
|
@@ -555,28 +630,28 @@ const endPreviewDrag = (event) => {
|
|
|
555
630
|
previewDragState.value = null;
|
|
556
631
|
};
|
|
557
632
|
|
|
558
|
-
const openFileInNewTab = (file, fileUrl) => {
|
|
559
|
-
if (fileUrl) {
|
|
560
|
-
window.open(fileUrl, '_blank');
|
|
561
|
-
return;
|
|
562
|
-
}
|
|
633
|
+
const openFileInNewTab = (file, fileUrl) => {
|
|
634
|
+
if (fileUrl) {
|
|
635
|
+
window.open(fileUrl, '_blank');
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
563
638
|
|
|
564
639
|
if (file.raw instanceof File || file.raw instanceof Blob) {
|
|
565
640
|
const blobUrl = URL.createObjectURL(file.raw);
|
|
566
641
|
const win = window.open(blobUrl, '_blank');
|
|
567
|
-
win?.addEventListener('unload', () => URL.revokeObjectURL(blobUrl));
|
|
568
|
-
}
|
|
569
|
-
};
|
|
570
|
-
|
|
571
|
-
const openCurrentPreview = () => {
|
|
572
|
-
if (!previewFile.value) return;
|
|
573
|
-
openFileInNewTab(previewFile.value, previewFile.value._previewSrc || previewFile.value.url || previewFile.value.preview);
|
|
574
|
-
};
|
|
575
|
-
|
|
576
|
-
const handlePreview = (file) => {
|
|
642
|
+
win?.addEventListener('unload', () => URL.revokeObjectURL(blobUrl));
|
|
643
|
+
}
|
|
644
|
+
};
|
|
645
|
+
|
|
646
|
+
const openCurrentPreview = () => {
|
|
647
|
+
if (!previewFile.value) return;
|
|
648
|
+
openFileInNewTab(previewFile.value, previewFile.value._previewSrc || previewFile.value.url || previewFile.value.preview);
|
|
649
|
+
};
|
|
650
|
+
|
|
651
|
+
const handlePreview = (file) => {
|
|
577
652
|
emit('preview', file);
|
|
578
653
|
|
|
579
|
-
const fileUrl = file
|
|
654
|
+
const fileUrl = getPreviewSrc(file);
|
|
580
655
|
|
|
581
656
|
// Non-image files open in a new tab.
|
|
582
657
|
if (!isImage(file) && !isExcel(file)) {
|
|
@@ -586,28 +661,34 @@ const handlePreview = (file) => {
|
|
|
586
661
|
|
|
587
662
|
// Excel:Office Online 查看器(需要文件有公网 URL)
|
|
588
663
|
if (isExcel(file)) {
|
|
589
|
-
if (fileUrl && /^https?:\/\//.test(fileUrl)) {
|
|
590
|
-
window.open(`https://view.officeapps.live.com/op/view.aspx?src=${encodeURIComponent(fileUrl)}`, '_blank');
|
|
591
|
-
} else {
|
|
592
|
-
// Excel without a public URL falls back to the dialog hint.
|
|
593
|
-
previewFile.value = { ...file, _previewSrc: fileUrl };
|
|
594
|
-
resetPreviewTransform();
|
|
595
|
-
previewVisible.value = true;
|
|
664
|
+
if (fileUrl && /^https?:\/\//.test(fileUrl)) {
|
|
665
|
+
window.open(`https://view.officeapps.live.com/op/view.aspx?src=${encodeURIComponent(fileUrl)}`, '_blank');
|
|
666
|
+
} else {
|
|
667
|
+
// Excel without a public URL falls back to the dialog hint.
|
|
668
|
+
previewFile.value = { ...file, _previewSrc: fileUrl };
|
|
669
|
+
resetPreviewTransform();
|
|
670
|
+
previewVisible.value = true;
|
|
596
671
|
}
|
|
597
672
|
return;
|
|
598
673
|
}
|
|
599
674
|
|
|
600
|
-
// Images preview inside the dialog.
|
|
601
|
-
|
|
602
|
-
...file,
|
|
603
|
-
_previewSrc: fileUrl,
|
|
604
|
-
};
|
|
605
|
-
resetPreviewTransform();
|
|
675
|
+
// Images preview inside the dialog.
|
|
676
|
+
setImagePreviewFile(file);
|
|
606
677
|
previewVisible.value = true;
|
|
607
678
|
};
|
|
608
679
|
|
|
609
680
|
watch(previewVisible, (visible) => {
|
|
610
|
-
if (
|
|
681
|
+
if (visible) {
|
|
682
|
+
window.addEventListener('keydown', handlePreviewKeydown);
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
window.removeEventListener('keydown', handlePreviewKeydown);
|
|
687
|
+
resetPreviewTransform();
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
onBeforeUnmount(() => {
|
|
691
|
+
window.removeEventListener('keydown', handlePreviewKeydown);
|
|
611
692
|
});
|
|
612
693
|
|
|
613
694
|
/* -------------------- 对外暴露方法 -------------------- */
|
|
@@ -729,7 +810,7 @@ defineExpose({ submit, clearFiles, remove });
|
|
|
729
810
|
.vtk-upload__picture-card-mask {
|
|
730
811
|
position: absolute;
|
|
731
812
|
inset: 0;
|
|
732
|
-
background: rgba(0, 0, 0, 0.
|
|
813
|
+
background: rgba(0, 0, 0, 0.08);
|
|
733
814
|
display: flex;
|
|
734
815
|
align-items: center;
|
|
735
816
|
justify-content: center;
|
|
@@ -768,16 +849,38 @@ defineExpose({ submit, clearFiles, remove });
|
|
|
768
849
|
|
|
769
850
|
/* ---- preview ---- */
|
|
770
851
|
.vtk-upload__preview-card {
|
|
852
|
+
--vtk-upload-preview-bg: rgba(0, 0, 0, 0.3);
|
|
853
|
+
|
|
771
854
|
height: 100vh;
|
|
772
|
-
background:
|
|
855
|
+
background: var(--vtk-upload-preview-bg);
|
|
773
856
|
}
|
|
774
857
|
|
|
775
858
|
.vtk-upload__preview-body {
|
|
776
859
|
height: calc(100vh - 64px);
|
|
777
860
|
overflow: auto;
|
|
778
|
-
background:
|
|
861
|
+
background: var(--vtk-upload-preview-bg);
|
|
862
|
+
position: relative;
|
|
779
863
|
}
|
|
780
864
|
|
|
865
|
+
.vtk-upload__preview-nav {
|
|
866
|
+
position: fixed;
|
|
867
|
+
top: 50%;
|
|
868
|
+
z-index: 10;
|
|
869
|
+
width: 50px;
|
|
870
|
+
height: 50px;
|
|
871
|
+
color: #fff;
|
|
872
|
+
background: rgba(0, 0, 0, 0.5);
|
|
873
|
+
transform: translateY(-50%);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
.vtk-upload__preview-nav--prev {
|
|
877
|
+
left: 20px;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
.vtk-upload__preview-nav--next {
|
|
881
|
+
right: 20px;
|
|
882
|
+
}
|
|
883
|
+
|
|
781
884
|
.vtk-upload__preview-image-wrap {
|
|
782
885
|
min-width: 100%;
|
|
783
886
|
min-height: 100%;
|
|
@@ -810,7 +913,7 @@ defineExpose({ submit, clearFiles, remove });
|
|
|
810
913
|
align-items: center;
|
|
811
914
|
justify-content: center;
|
|
812
915
|
padding: 32px;
|
|
813
|
-
color: rgba(
|
|
916
|
+
color: rgba(var(--v-theme-on-surface), 0.72);
|
|
814
917
|
text-align: center;
|
|
815
918
|
}
|
|
816
919
|
</style>
|