@yxhl/specter-pui-vtk 1.0.85 → 1.0.87

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.85",
3
+ "version": "1.0.87",
4
4
  "description": "雅心互联 Vue 3 + Vuetify3 组件库",
5
5
  "type": "module",
6
6
  "main": "./dist/specter-pui.umd.js",
@@ -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
@@ -196,18 +196,22 @@ const srcWithToken = computed(() => {
196
196
  const currentImageUrl = computed(() => {
197
197
  if (props.imageList.length > 0 && currentIndex.value < props.imageList.length) {
198
198
  const currentImage = props.imageList[currentIndex.value];
199
- if (currentImage && currentImage.url) {
200
- try {
201
- // 为图片列表中的图片也添加token
202
- if (window.$vtk && typeof window.$vtk.storage?.get === 'function') {
203
- const token = window.$vtk.storage.get('_mis_acis_token');
204
- return token ? `${currentImage.url}?stoken=${token}` : currentImage.url;
205
- } else {
206
- const token = localStorage.getItem('_mis_acis_token');
207
- return token ? `${currentImage.url}?stoken=${token}` : currentImage.url;
199
+ if (currentImage) {
200
+ // 支持 imageList 项为字符串(直接是URL)或对象(含 url 属性)
201
+ const url = typeof currentImage === 'string' ? currentImage : currentImage?.url;
202
+ if (url) {
203
+ try {
204
+ // 为图片列表中的图片也添加token
205
+ if (window.$vtk && typeof window.$vtk.storage?.get === 'function') {
206
+ const token = window.$vtk.storage.get('_mis_acis_token');
207
+ return token ? `${url}?stoken=${token}` : url;
208
+ } else {
209
+ const token = localStorage.getItem('_mis_acis_token');
210
+ return token ? `${url}?stoken=${token}` : url;
211
+ }
212
+ } catch (error) {
213
+ return url;
208
214
  }
209
- } catch (error) {
210
- return currentImage.url;
211
215
  }
212
216
  }
213
217
  }
@@ -379,7 +383,7 @@ const endImageDrag = (event) => {
379
383
 
380
384
  /* 图片查看器样式 */
381
385
  .image-viewer-card {
382
- background-color: rgba(0, 0, 0, 0.9);
386
+ background-color: rgba(0, 0, 0, 0.3);
383
387
  color: white;
384
388
  }
385
389
 
@@ -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="缩小" @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>
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.url || file.preview;
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
- previewFile.value = {
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 (!visible) resetPreviewTransform();
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.45);
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: #111;
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: #111;
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(255, 255, 255, 0.72);
916
+ color: rgba(var(--v-theme-on-surface), 0.72);
814
917
  text-align: center;
815
918
  }
816
919
  </style>