@yxhl/specter-pui-vtk 1.0.82 → 1.0.84
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 +2545 -2473
- 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/VtkUpload.vue +161 -86
package/package.json
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
<div class="vtk-upload">
|
|
3
3
|
<!-- 拖拽 / 点击上传区域 -->
|
|
4
4
|
<div
|
|
5
|
-
v-if="listType !== 'picture-card'"
|
|
6
|
-
:class="['vtk-upload__dragger', { 'is-dragover': isDragover, 'is-disabled': disabled }]"
|
|
7
|
-
|
|
5
|
+
v-if="listType !== 'picture-card' && !detail"
|
|
6
|
+
:class="['vtk-upload__dragger', { 'is-dragover': isDragover, 'is-disabled': disabled }]"
|
|
7
|
+
title="点击上传"
|
|
8
|
+
@click="!disabled && triggerInput()"
|
|
8
9
|
@dragover.prevent="onDragover"
|
|
9
10
|
@dragleave.prevent="isDragover = false"
|
|
10
11
|
@drop.prevent="onDrop"
|
|
@@ -27,12 +28,12 @@
|
|
|
27
28
|
>
|
|
28
29
|
<v-img :src="file.url || file.preview" cover class="fill-height" />
|
|
29
30
|
<div class="vtk-upload__picture-card-mask">
|
|
30
|
-
<VBtn
|
|
31
|
-
<VIcon>mdi-eye</VIcon>
|
|
32
|
-
</VBtn>
|
|
33
|
-
<VBtn v-if="!disabled" icon size="x-small" variant="text" color="white" @click.stop="handleRemove(file)">
|
|
34
|
-
<VIcon>mdi-delete</VIcon>
|
|
35
|
-
</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>
|
|
36
37
|
</div>
|
|
37
38
|
<!-- 上传进度 -->
|
|
38
39
|
<div v-if="file.status === 'uploading'" class="vtk-upload__picture-card-progress">
|
|
@@ -42,9 +43,10 @@
|
|
|
42
43
|
|
|
43
44
|
<!-- 添加按钮 -->
|
|
44
45
|
<div
|
|
45
|
-
v-if="!disabled && (limit === 0 || fileList.length < limit)"
|
|
46
|
-
:class="['vtk-upload__picture-card-add', { 'is-dragover': isDragover }]"
|
|
47
|
-
|
|
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()"
|
|
48
50
|
@dragover.prevent="onDragover"
|
|
49
51
|
@dragleave.prevent="isDragover = false"
|
|
50
52
|
@drop.prevent="onDrop"
|
|
@@ -77,13 +79,14 @@
|
|
|
77
79
|
<span class="vtk-upload__list-item-name flex-grow-1 text-truncate" @click="handlePreview(file)" :title="file.name">{{ file.name }}</span>
|
|
78
80
|
<span v-if="file.status === 'uploading'" class="text-caption text-grey ml-2">{{ file.percentage }}%</span>
|
|
79
81
|
<VBtn
|
|
80
|
-
v-if="!disabled"
|
|
82
|
+
v-if="!disabled && !detail"
|
|
81
83
|
icon
|
|
82
84
|
size="x-small"
|
|
83
|
-
variant="text"
|
|
84
|
-
color="grey"
|
|
85
|
-
class="ml-1"
|
|
86
|
-
|
|
85
|
+
variant="text"
|
|
86
|
+
color="grey"
|
|
87
|
+
class="ml-1"
|
|
88
|
+
title="删除"
|
|
89
|
+
@click="handleRemove(file)"
|
|
87
90
|
>
|
|
88
91
|
<VIcon size="16">mdi-close</VIcon>
|
|
89
92
|
</VBtn>
|
|
@@ -102,28 +105,31 @@
|
|
|
102
105
|
<VDialog v-model="previewVisible" fullscreen scrollable @click:outside="closePreview">
|
|
103
106
|
<VCard class="vtk-upload__preview-card">
|
|
104
107
|
<v-toolbar color="primary" density="comfortable">
|
|
105
|
-
<v-toolbar-title class="text-truncate">{{ previewFile?.name }}</v-toolbar-title>
|
|
106
|
-
<VSpacer />
|
|
107
|
-
<
|
|
108
|
-
<
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
<VBtn icon variant="text" @click="zoomPreview(0.3)">
|
|
112
|
-
<VIcon>mdi-magnify-
|
|
113
|
-
</VBtn>
|
|
114
|
-
<VBtn icon variant="text" @click="
|
|
115
|
-
<VIcon>mdi-
|
|
116
|
-
</VBtn>
|
|
117
|
-
<VBtn icon variant="text" @click="rotatePreview(90)">
|
|
118
|
-
<VIcon>mdi-rotate-
|
|
119
|
-
</VBtn>
|
|
120
|
-
<VBtn icon variant="text" @click="
|
|
121
|
-
<VIcon>mdi-
|
|
122
|
-
</VBtn>
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
</
|
|
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="缩小" @click="zoomPreview(-0.3)">
|
|
115
|
+
<VIcon>mdi-magnify-minus-outline</VIcon>
|
|
116
|
+
</VBtn>
|
|
117
|
+
<VBtn icon variant="text" title="放大" @click="zoomPreview(0.3)">
|
|
118
|
+
<VIcon>mdi-magnify-plus-outline</VIcon>
|
|
119
|
+
</VBtn>
|
|
120
|
+
<VBtn icon variant="text" title="向左旋转" @click="rotatePreview(-90)">
|
|
121
|
+
<VIcon>mdi-rotate-left</VIcon>
|
|
122
|
+
</VBtn>
|
|
123
|
+
<VBtn icon variant="text" title="向右旋转" @click="rotatePreview(90)">
|
|
124
|
+
<VIcon>mdi-rotate-right</VIcon>
|
|
125
|
+
</VBtn>
|
|
126
|
+
<VBtn icon variant="text" title="重置" @click="resetPreviewTransform">
|
|
127
|
+
<VIcon>mdi-refresh</VIcon>
|
|
128
|
+
</VBtn>
|
|
129
|
+
</template>
|
|
130
|
+
<VBtn icon variant="text" title="关闭" @click="closePreview">
|
|
131
|
+
<VIcon>mdi-close</VIcon>
|
|
132
|
+
</VBtn>
|
|
127
133
|
</v-toolbar>
|
|
128
134
|
<div class="vtk-upload__preview-body">
|
|
129
135
|
<div v-if="isImage(previewFile)" class="vtk-upload__preview-image-wrap">
|
|
@@ -148,7 +154,7 @@
|
|
|
148
154
|
</template>
|
|
149
155
|
|
|
150
156
|
<script setup>
|
|
151
|
-
import { computed, ref, watch } from 'vue';
|
|
157
|
+
import { computed, nextTick, ref, watch } from 'vue';
|
|
152
158
|
import Request from '../../commons/request.js';
|
|
153
159
|
|
|
154
160
|
defineOptions({
|
|
@@ -204,10 +210,15 @@ const props = defineProps({
|
|
|
204
210
|
default: true,
|
|
205
211
|
},
|
|
206
212
|
/** 是否禁用 */
|
|
207
|
-
disabled: {
|
|
208
|
-
type: Boolean,
|
|
209
|
-
default: false,
|
|
210
|
-
},
|
|
213
|
+
disabled: {
|
|
214
|
+
type: Boolean,
|
|
215
|
+
default: false,
|
|
216
|
+
},
|
|
217
|
+
/** 详情模式:隐藏上传和删除入口,仅展示已上传文件并保留查看 */
|
|
218
|
+
detail: {
|
|
219
|
+
type: Boolean,
|
|
220
|
+
default: false,
|
|
221
|
+
},
|
|
211
222
|
/** 附加请求头 */
|
|
212
223
|
headers: {
|
|
213
224
|
type: Object,
|
|
@@ -262,23 +273,77 @@ const previewTranslateX = ref(0);
|
|
|
262
273
|
const previewTranslateY = ref(0);
|
|
263
274
|
const previewDragState = ref(null);
|
|
264
275
|
|
|
265
|
-
// 内部维护文件列表
|
|
266
|
-
const fileList = ref([
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
(
|
|
271
|
-
|
|
272
|
-
if (
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
+
);
|
|
276
292
|
|
|
277
293
|
/* -------------------- 工具函数 -------------------- */
|
|
278
|
-
let uidCounter = 0;
|
|
279
|
-
const genUid = () => `vtk-upload-${Date.now()}-${uidCounter++}`;
|
|
280
|
-
|
|
281
|
-
const
|
|
294
|
+
let uidCounter = 0;
|
|
295
|
+
const genUid = () => `vtk-upload-${Date.now()}-${uidCounter++}`;
|
|
296
|
+
|
|
297
|
+
const getFileNameFromUrl = (url) => {
|
|
298
|
+
const cleanUrl = String(url || '').split('?')[0].split('#')[0];
|
|
299
|
+
const name = cleanUrl.substring(cleanUrl.lastIndexOf('/') + 1);
|
|
300
|
+
try {
|
|
301
|
+
return decodeURIComponent(name || cleanUrl || 'file');
|
|
302
|
+
} catch (e) {
|
|
303
|
+
return name || cleanUrl || 'file';
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const normalizeFileList = (value) => {
|
|
308
|
+
if (!Array.isArray(value)) return [];
|
|
309
|
+
|
|
310
|
+
return value
|
|
311
|
+
.filter((item) => item)
|
|
312
|
+
.map((item) => {
|
|
313
|
+
if (typeof item === 'string') {
|
|
314
|
+
return {
|
|
315
|
+
uid: genUid(),
|
|
316
|
+
name: getFileNameFromUrl(item),
|
|
317
|
+
size: 0,
|
|
318
|
+
type: '',
|
|
319
|
+
status: 'success',
|
|
320
|
+
percentage: 100,
|
|
321
|
+
raw: null,
|
|
322
|
+
url: item,
|
|
323
|
+
preview: '',
|
|
324
|
+
response: null,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return {
|
|
329
|
+
uid: item.uid || genUid(),
|
|
330
|
+
name: item.name || getFileNameFromUrl(item.url || item.preview),
|
|
331
|
+
size: item.size || 0,
|
|
332
|
+
type: item.type || '',
|
|
333
|
+
status: item.status || 'success',
|
|
334
|
+
percentage: item.percentage ?? 100,
|
|
335
|
+
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) => {
|
|
282
347
|
if (!file) return false;
|
|
283
348
|
return /image\//.test(file.type) || /\.(png|jpg|jpeg|gif|bmp|webp|svg)$/i.test(file.name || '');
|
|
284
349
|
};
|
|
@@ -299,19 +364,20 @@ const triggerInput = () => {
|
|
|
299
364
|
};
|
|
300
365
|
|
|
301
366
|
/* -------------------- 拖拽 -------------------- */
|
|
302
|
-
const onDragover = () => {
|
|
303
|
-
if (!props.disabled) isDragover.value = true;
|
|
304
|
-
};
|
|
305
|
-
|
|
306
|
-
const onDrop = (e) => {
|
|
307
|
-
isDragover.value = false;
|
|
308
|
-
if (props.disabled) return;
|
|
309
|
-
processFiles(Array.from(e.dataTransfer.files));
|
|
310
|
-
};
|
|
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
|
+
};
|
|
311
376
|
|
|
312
377
|
/* -------------------- input change -------------------- */
|
|
313
|
-
const onInputChange = (e) => {
|
|
314
|
-
|
|
378
|
+
const onInputChange = (e) => {
|
|
379
|
+
if (props.disabled || props.detail) return;
|
|
380
|
+
processFiles(Array.from(e.target.files));
|
|
315
381
|
// 清空,允许重复选同一文件
|
|
316
382
|
e.target.value = '';
|
|
317
383
|
};
|
|
@@ -373,12 +439,16 @@ const addFile = (file) => {
|
|
|
373
439
|
emit('change', file, fileList.value);
|
|
374
440
|
};
|
|
375
441
|
|
|
376
|
-
const syncModel = () => {
|
|
377
|
-
const urls = fileList.value
|
|
378
|
-
.filter((f) => f.status === 'success' && f.url)
|
|
379
|
-
.map((f) => String(f.url));
|
|
380
|
-
|
|
381
|
-
|
|
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
|
+
};
|
|
382
452
|
|
|
383
453
|
/* -------------------- 上传逻辑 -------------------- */
|
|
384
454
|
const uploadFile = (file) => {
|
|
@@ -485,20 +555,25 @@ const endPreviewDrag = (event) => {
|
|
|
485
555
|
previewDragState.value = null;
|
|
486
556
|
};
|
|
487
557
|
|
|
488
|
-
const openFileInNewTab = (file, fileUrl) => {
|
|
489
|
-
if (fileUrl) {
|
|
490
|
-
window.open(fileUrl, '_blank');
|
|
491
|
-
return;
|
|
492
|
-
}
|
|
558
|
+
const openFileInNewTab = (file, fileUrl) => {
|
|
559
|
+
if (fileUrl) {
|
|
560
|
+
window.open(fileUrl, '_blank');
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
493
563
|
|
|
494
564
|
if (file.raw instanceof File || file.raw instanceof Blob) {
|
|
495
565
|
const blobUrl = URL.createObjectURL(file.raw);
|
|
496
566
|
const win = window.open(blobUrl, '_blank');
|
|
497
|
-
win?.addEventListener('unload', () => URL.revokeObjectURL(blobUrl));
|
|
498
|
-
}
|
|
499
|
-
};
|
|
500
|
-
|
|
501
|
-
const
|
|
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) => {
|
|
502
577
|
emit('preview', file);
|
|
503
578
|
|
|
504
579
|
const fileUrl = file.url || file.preview;
|