@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/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.84",
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">
@@ -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([...(props.modelValue || [])]);
267
-
268
- watch(
269
- () => props.modelValue,
270
- (val) => {
271
- // 外部传入字符串数组(url 列表)时跳过,避免覆盖内部状态
272
- if (!val?.length || typeof val[0] === 'string') return;
273
- fileList.value = [...val];
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 isImage = (file) => {
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
- processFiles(Array.from(e.target.files));
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
- emit('update:modelValue', urls);
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 handlePreview = (file) => {
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;