@yxhl/specter-pui-vtk 1.0.82 → 1.0.83

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.82",
3
+ "version": "1.0.83",
4
4
  "description": "雅心互联 Vue 3 + Vuetify3 组件库",
5
5
  "type": "module",
6
6
  "main": "./dist/specter-pui.umd.js",
@@ -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
- @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()"
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 v-if="!disabled" icon size="x-small" variant="text" color="white" @click.stop="handlePreview(file)">
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
- @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()"
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
- @click="handleRemove(file)"
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
- <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>
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">
@@ -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,
@@ -263,22 +274,70 @@ const previewTranslateY = ref(0);
263
274
  const previewDragState = ref(null);
264
275
 
265
276
  // 内部维护文件列表
266
- const fileList = ref([...(props.modelValue || [])]);
277
+ const fileList = ref([]);
267
278
 
268
279
  watch(
269
280
  () => props.modelValue,
270
281
  (val) => {
271
- // 外部传入字符串数组(url 列表)时跳过,避免覆盖内部状态
272
- if (!val?.length || typeof val[0] === 'string') return;
273
- fileList.value = [...val];
282
+ // 统一接收外部传入的 URL 列表或文件对象列表。
283
+ fileList.value = normalizeFileList(val || []);
274
284
  },
275
285
  );
276
286
 
277
287
  /* -------------------- 工具函数 -------------------- */
278
- let uidCounter = 0;
279
- const genUid = () => `vtk-upload-${Date.now()}-${uidCounter++}`;
280
-
281
- const isImage = (file) => {
288
+ let uidCounter = 0;
289
+ const genUid = () => `vtk-upload-${Date.now()}-${uidCounter++}`;
290
+
291
+ const getFileNameFromUrl = (url) => {
292
+ const cleanUrl = String(url || '').split('?')[0].split('#')[0];
293
+ const name = cleanUrl.substring(cleanUrl.lastIndexOf('/') + 1);
294
+ try {
295
+ return decodeURIComponent(name || cleanUrl || 'file');
296
+ } catch (e) {
297
+ return name || cleanUrl || 'file';
298
+ }
299
+ };
300
+
301
+ const normalizeFileList = (value) => {
302
+ if (!Array.isArray(value)) return [];
303
+
304
+ return value
305
+ .filter((item) => item)
306
+ .map((item) => {
307
+ if (typeof item === 'string') {
308
+ return {
309
+ uid: genUid(),
310
+ name: getFileNameFromUrl(item),
311
+ size: 0,
312
+ type: '',
313
+ status: 'success',
314
+ percentage: 100,
315
+ raw: null,
316
+ url: item,
317
+ preview: '',
318
+ response: null,
319
+ };
320
+ }
321
+
322
+ return {
323
+ uid: item.uid || genUid(),
324
+ name: item.name || getFileNameFromUrl(item.url || item.preview),
325
+ size: item.size || 0,
326
+ type: item.type || '',
327
+ status: item.status || 'success',
328
+ percentage: item.percentage ?? 100,
329
+ raw: item.raw || null,
330
+ url: item.url || '',
331
+ preview: item.preview || '',
332
+ response: item.response || null,
333
+ ...item,
334
+ };
335
+ });
336
+ };
337
+
338
+ fileList.value = normalizeFileList(props.modelValue || []);
339
+
340
+ const isImage = (file) => {
282
341
  if (!file) return false;
283
342
  return /image\//.test(file.type) || /\.(png|jpg|jpeg|gif|bmp|webp|svg)$/i.test(file.name || '');
284
343
  };
@@ -299,19 +358,20 @@ const triggerInput = () => {
299
358
  };
300
359
 
301
360
  /* -------------------- 拖拽 -------------------- */
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
- };
361
+ const onDragover = () => {
362
+ if (!props.disabled && !props.detail) isDragover.value = true;
363
+ };
364
+
365
+ const onDrop = (e) => {
366
+ isDragover.value = false;
367
+ if (props.disabled || props.detail) return;
368
+ processFiles(Array.from(e.dataTransfer.files));
369
+ };
311
370
 
312
371
  /* -------------------- input change -------------------- */
313
- const onInputChange = (e) => {
314
- processFiles(Array.from(e.target.files));
372
+ const onInputChange = (e) => {
373
+ if (props.disabled || props.detail) return;
374
+ processFiles(Array.from(e.target.files));
315
375
  // 清空,允许重复选同一文件
316
376
  e.target.value = '';
317
377
  };
@@ -485,20 +545,25 @@ const endPreviewDrag = (event) => {
485
545
  previewDragState.value = null;
486
546
  };
487
547
 
488
- const openFileInNewTab = (file, fileUrl) => {
489
- if (fileUrl) {
490
- window.open(fileUrl, '_blank');
491
- return;
492
- }
548
+ const openFileInNewTab = (file, fileUrl) => {
549
+ if (fileUrl) {
550
+ window.open(fileUrl, '_blank');
551
+ return;
552
+ }
493
553
 
494
554
  if (file.raw instanceof File || file.raw instanceof Blob) {
495
555
  const blobUrl = URL.createObjectURL(file.raw);
496
556
  const win = window.open(blobUrl, '_blank');
497
- win?.addEventListener('unload', () => URL.revokeObjectURL(blobUrl));
498
- }
499
- };
500
-
501
- const handlePreview = (file) => {
557
+ win?.addEventListener('unload', () => URL.revokeObjectURL(blobUrl));
558
+ }
559
+ };
560
+
561
+ const openCurrentPreview = () => {
562
+ if (!previewFile.value) return;
563
+ openFileInNewTab(previewFile.value, previewFile.value._previewSrc || previewFile.value.url || previewFile.value.preview);
564
+ };
565
+
566
+ const handlePreview = (file) => {
502
567
  emit('preview', file);
503
568
 
504
569
  const fileUrl = file.url || file.preview;