@yxhl/specter-pui-vtk 1.0.89 → 1.0.90
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 +1280 -1290
- 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 +262 -276
package/package.json
CHANGED
|
@@ -41,15 +41,15 @@
|
|
|
41
41
|
<v-toolbar-title>{{ currentImageTitle }}</v-toolbar-title>
|
|
42
42
|
<VSpacer></VSpacer>
|
|
43
43
|
<v-toolbar-items>
|
|
44
|
-
<VBtn icon dark title="新窗口打开" @click="openCurrentImageInNewWindow">
|
|
45
|
-
<VIcon>mdi-open-in-new</VIcon>
|
|
46
|
-
</VBtn>
|
|
47
|
-
<VBtn icon dark title="下载图片" @click="downloadCurrentImage">
|
|
48
|
-
<VIcon>mdi-download</VIcon>
|
|
49
|
-
</VBtn>
|
|
50
|
-
<VBtn icon dark @click="zoomImage(-0.3)">
|
|
51
|
-
<VIcon>mdi-magnify-minus-outline</VIcon>
|
|
52
|
-
</VBtn>
|
|
44
|
+
<VBtn icon dark title="新窗口打开" @click="openCurrentImageInNewWindow">
|
|
45
|
+
<VIcon>mdi-open-in-new</VIcon>
|
|
46
|
+
</VBtn>
|
|
47
|
+
<VBtn icon dark title="下载图片" @click="downloadCurrentImage">
|
|
48
|
+
<VIcon>mdi-download</VIcon>
|
|
49
|
+
</VBtn>
|
|
50
|
+
<VBtn icon dark @click="zoomImage(-0.3)">
|
|
51
|
+
<VIcon>mdi-magnify-minus-outline</VIcon>
|
|
52
|
+
</VBtn>
|
|
53
53
|
<VBtn icon dark @click="zoomImage(0.3)">
|
|
54
54
|
<VIcon>mdi-magnify-plus-outline</VIcon>
|
|
55
55
|
</VBtn>
|
|
@@ -108,28 +108,28 @@
|
|
|
108
108
|
</div>
|
|
109
109
|
</template>
|
|
110
110
|
|
|
111
|
-
<script setup>
|
|
112
|
-
import { ref, computed, watch, getCurrentInstance } from 'vue';
|
|
111
|
+
<script setup>
|
|
112
|
+
import { ref, computed, watch, getCurrentInstance } from 'vue';
|
|
113
113
|
|
|
114
|
-
defineOptions({
|
|
115
|
-
name: 'VtkImg',
|
|
116
|
-
inheritAttrs: true
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
const { proxy } = getCurrentInstance();
|
|
114
|
+
defineOptions({
|
|
115
|
+
name: 'VtkImg',
|
|
116
|
+
inheritAttrs: true
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const { proxy } = getCurrentInstance();
|
|
120
120
|
|
|
121
121
|
const props = defineProps({
|
|
122
|
-
src: {
|
|
123
|
-
type: String,
|
|
124
|
-
default: null,
|
|
125
|
-
},
|
|
126
|
-
downloadSrc: {
|
|
127
|
-
type: String,
|
|
128
|
-
default: '',
|
|
129
|
-
},
|
|
130
|
-
error: {
|
|
131
|
-
type: String,
|
|
132
|
-
default: new URL('../../assets/img/wxsq.png', import.meta.url).href,
|
|
122
|
+
src: {
|
|
123
|
+
type: String,
|
|
124
|
+
default: null,
|
|
125
|
+
},
|
|
126
|
+
downloadSrc: {
|
|
127
|
+
type: String,
|
|
128
|
+
default: '',
|
|
129
|
+
},
|
|
130
|
+
error: {
|
|
131
|
+
type: String,
|
|
132
|
+
default: new URL('../../assets/img/wxsq.png', import.meta.url).href,
|
|
133
133
|
},
|
|
134
134
|
preview: {
|
|
135
135
|
type: Boolean,
|
|
@@ -227,32 +227,32 @@ const currentImageUrl = computed(() => {
|
|
|
227
227
|
}
|
|
228
228
|
}
|
|
229
229
|
}
|
|
230
|
-
return srcWithToken.value;
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
const currentImageRawUrl = computed(() => {
|
|
234
|
-
if (props.imageList.length > 0 && currentIndex.value < props.imageList.length) {
|
|
235
|
-
const currentImage = props.imageList[currentIndex.value];
|
|
236
|
-
const url = typeof currentImage === 'string' ? currentImage : currentImage?.url;
|
|
237
|
-
return url || props.src;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
return props.src;
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
const currentImageDownloadUrl = computed(() => {
|
|
244
|
-
if (props.downloadSrc) return props.downloadSrc;
|
|
245
|
-
|
|
246
|
-
if (props.imageList.length > 0 && currentIndex.value < props.imageList.length) {
|
|
247
|
-
const currentImage = props.imageList[currentIndex.value];
|
|
248
|
-
if (currentImage && typeof currentImage === 'object') {
|
|
249
|
-
return currentImage.downloadUrl || currentImage.downloadSrc || currentImage.url || props.src;
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
return currentImageRawUrl.value;
|
|
254
|
-
});
|
|
255
|
-
|
|
230
|
+
return srcWithToken.value;
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const currentImageRawUrl = computed(() => {
|
|
234
|
+
if (props.imageList.length > 0 && currentIndex.value < props.imageList.length) {
|
|
235
|
+
const currentImage = props.imageList[currentIndex.value];
|
|
236
|
+
const url = typeof currentImage === 'string' ? currentImage : currentImage?.url;
|
|
237
|
+
return url || props.src;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return props.src;
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
const currentImageDownloadUrl = computed(() => {
|
|
244
|
+
if (props.downloadSrc) return props.downloadSrc;
|
|
245
|
+
|
|
246
|
+
if (props.imageList.length > 0 && currentIndex.value < props.imageList.length) {
|
|
247
|
+
const currentImage = props.imageList[currentIndex.value];
|
|
248
|
+
if (currentImage && typeof currentImage === 'object') {
|
|
249
|
+
return currentImage.downloadUrl || currentImage.downloadSrc || currentImage.url || props.src;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return currentImageRawUrl.value;
|
|
254
|
+
});
|
|
255
|
+
|
|
256
256
|
// 当前图片标题
|
|
257
257
|
const currentImageTitle = computed(() => {
|
|
258
258
|
if (props.imageList.length > 0 && currentIndex.value < props.imageList.length) {
|
|
@@ -285,228 +285,214 @@ const viewerImageStyle = computed(() => ({
|
|
|
285
285
|
transform: `translate(${imageTranslateX.value}px, ${imageTranslateY.value}px) scale(${imageScale.value}) rotate(${imageRotation.value}deg)`,
|
|
286
286
|
}));
|
|
287
287
|
|
|
288
|
-
const openCurrentImageInNewWindow = () => {
|
|
289
|
-
if (!currentImageRawUrl.value) return;
|
|
290
|
-
|
|
291
|
-
window.open(currentImageRawUrl.value, '_blank', 'noopener');
|
|
292
|
-
};
|
|
293
|
-
|
|
294
|
-
const imageMimeExtensionMap = {
|
|
295
|
-
'image/bmp': 'bmp',
|
|
296
|
-
'image/gif': 'gif',
|
|
297
|
-
'image/jpeg': 'jpg',
|
|
298
|
-
'image/png': 'png',
|
|
299
|
-
'image/svg+xml': 'svg',
|
|
300
|
-
'image/webp': 'webp',
|
|
301
|
-
};
|
|
302
|
-
|
|
303
|
-
const imageExtensionMimeMap = {
|
|
304
|
-
bmp: 'image/bmp',
|
|
305
|
-
gif: 'image/gif',
|
|
306
|
-
jpeg: 'image/jpeg',
|
|
307
|
-
jpg: 'image/jpeg',
|
|
308
|
-
png: 'image/png',
|
|
309
|
-
svg: 'image/svg+xml',
|
|
310
|
-
webp: 'image/webp',
|
|
311
|
-
};
|
|
312
|
-
|
|
313
|
-
const getImageFileExtension = (blob) => {
|
|
314
|
-
const mimeType = blob?.type?.split(';')[0]?.toLowerCase();
|
|
315
|
-
|
|
316
|
-
if (mimeType && imageMimeExtensionMap[mimeType]) {
|
|
317
|
-
return imageMimeExtensionMap[mimeType];
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
try {
|
|
321
|
-
const { pathname } = new URL(currentImageRawUrl.value, window.location.href);
|
|
322
|
-
const matched = pathname.match(/\.([a-z0-9]+)$/i);
|
|
323
|
-
return matched?.[1]?.toLowerCase() || 'png';
|
|
324
|
-
} catch (error) {
|
|
325
|
-
return 'png';
|
|
326
|
-
}
|
|
327
|
-
};
|
|
328
|
-
|
|
329
|
-
const getCurrentImageFileName = (blob) => {
|
|
330
|
-
const extension = getImageFileExtension(blob);
|
|
331
|
-
const titleName = currentImageTitle.value?.trim();
|
|
332
|
-
|
|
333
|
-
if (titleName) {
|
|
334
|
-
const safeTitle = titleName.replace(/[\\/:*?"<>|]/g, '_').replace(/\.[a-z0-9]+$/i, '');
|
|
335
|
-
return `${safeTitle}.${extension}`;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
try {
|
|
339
|
-
const { pathname } = new URL(currentImageRawUrl.value, window.location.href);
|
|
340
|
-
const urlName = decodeURIComponent(pathname.split('/').pop() || '')
|
|
341
|
-
.replace(/[\\/:*?"<>|]/g, '_')
|
|
342
|
-
.replace(/\.[a-z0-9]+$/i, '');
|
|
343
|
-
return urlName ? `${urlName}.${extension}` : `image.${extension}`;
|
|
344
|
-
} catch (error) {
|
|
345
|
-
return `image.${extension}`;
|
|
346
|
-
}
|
|
347
|
-
};
|
|
348
|
-
|
|
349
|
-
const triggerBrowserDownload = (blob, fileName) => {
|
|
350
|
-
const url = window.URL.createObjectURL(blob);
|
|
351
|
-
const link = document.createElement('a');
|
|
352
|
-
link.href = url;
|
|
353
|
-
link.download = fileName;
|
|
354
|
-
document.body.appendChild(link);
|
|
355
|
-
link.click();
|
|
356
|
-
document.body.removeChild(link);
|
|
357
|
-
window.URL.revokeObjectURL(url);
|
|
358
|
-
};
|
|
359
|
-
|
|
360
|
-
const triggerBrowserUrlDownload = (url, fileName) => {
|
|
361
|
-
const link = document.createElement('a');
|
|
362
|
-
link.href = url;
|
|
363
|
-
link.download = fileName;
|
|
364
|
-
link.target = '_blank';
|
|
365
|
-
link.rel = 'noopener';
|
|
366
|
-
document.body.appendChild(link);
|
|
367
|
-
link.click();
|
|
368
|
-
document.body.removeChild(link);
|
|
369
|
-
};
|
|
370
|
-
|
|
371
|
-
const showDownloadToast = (message, color = 'info') => {
|
|
372
|
-
try {
|
|
373
|
-
const toast = proxy?.$vtk?.message?.toast || window.$vtk?.message?.toast;
|
|
374
|
-
if (typeof toast === 'function') {
|
|
375
|
-
toast(message, { color });
|
|
376
|
-
return;
|
|
377
|
-
}
|
|
378
|
-
} catch (error) {
|
|
379
|
-
console.warn('VtkImg: failed to show message toast', error);
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
if (color === 'error' || color === 'warning') {
|
|
383
|
-
window.alert?.(message);
|
|
384
|
-
}
|
|
385
|
-
};
|
|
386
|
-
|
|
387
|
-
const
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
const
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
throw new Error(
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
return
|
|
460
|
-
};
|
|
461
|
-
|
|
462
|
-
const
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
triggerBrowserDownload(blob, fileName);
|
|
499
|
-
showDownloadToast('图片已开始下载。', 'success');
|
|
500
|
-
} catch (error) {
|
|
501
|
-
console.error('下载图片失败:图片地址不允许跨域读取,请配置资源 CORS 或传入同源 downloadSrc。', error);
|
|
502
|
-
triggerBrowserUrlDownload(downloadUrl, getCurrentImageFileName());
|
|
503
|
-
showDownloadToast('图片下载失败,已切换为浏览器下载。', 'warning');
|
|
504
|
-
}
|
|
505
|
-
};
|
|
506
|
-
|
|
507
|
-
const resetImageTransform = () => {
|
|
508
|
-
imageRotation.value = 0;
|
|
509
|
-
imageScale.value = 1;
|
|
288
|
+
const openCurrentImageInNewWindow = () => {
|
|
289
|
+
if (!currentImageRawUrl.value) return;
|
|
290
|
+
|
|
291
|
+
window.open(currentImageRawUrl.value, '_blank', 'noopener');
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
const imageMimeExtensionMap = {
|
|
295
|
+
'image/bmp': 'bmp',
|
|
296
|
+
'image/gif': 'gif',
|
|
297
|
+
'image/jpeg': 'jpg',
|
|
298
|
+
'image/png': 'png',
|
|
299
|
+
'image/svg+xml': 'svg',
|
|
300
|
+
'image/webp': 'webp',
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const imageExtensionMimeMap = {
|
|
304
|
+
bmp: 'image/bmp',
|
|
305
|
+
gif: 'image/gif',
|
|
306
|
+
jpeg: 'image/jpeg',
|
|
307
|
+
jpg: 'image/jpeg',
|
|
308
|
+
png: 'image/png',
|
|
309
|
+
svg: 'image/svg+xml',
|
|
310
|
+
webp: 'image/webp',
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
const getImageFileExtension = (blob) => {
|
|
314
|
+
const mimeType = blob?.type?.split(';')[0]?.toLowerCase();
|
|
315
|
+
|
|
316
|
+
if (mimeType && imageMimeExtensionMap[mimeType]) {
|
|
317
|
+
return imageMimeExtensionMap[mimeType];
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
try {
|
|
321
|
+
const { pathname } = new URL(currentImageRawUrl.value, window.location.href);
|
|
322
|
+
const matched = pathname.match(/\.([a-z0-9]+)$/i);
|
|
323
|
+
return matched?.[1]?.toLowerCase() || 'png';
|
|
324
|
+
} catch (error) {
|
|
325
|
+
return 'png';
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
const getCurrentImageFileName = (blob) => {
|
|
330
|
+
const extension = getImageFileExtension(blob);
|
|
331
|
+
const titleName = currentImageTitle.value?.trim();
|
|
332
|
+
|
|
333
|
+
if (titleName) {
|
|
334
|
+
const safeTitle = titleName.replace(/[\\/:*?"<>|]/g, '_').replace(/\.[a-z0-9]+$/i, '');
|
|
335
|
+
return `${safeTitle}.${extension}`;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
try {
|
|
339
|
+
const { pathname } = new URL(currentImageRawUrl.value, window.location.href);
|
|
340
|
+
const urlName = decodeURIComponent(pathname.split('/').pop() || '')
|
|
341
|
+
.replace(/[\\/:*?"<>|]/g, '_')
|
|
342
|
+
.replace(/\.[a-z0-9]+$/i, '');
|
|
343
|
+
return urlName ? `${urlName}.${extension}` : `image.${extension}`;
|
|
344
|
+
} catch (error) {
|
|
345
|
+
return `image.${extension}`;
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
const triggerBrowserDownload = (blob, fileName) => {
|
|
350
|
+
const url = window.URL.createObjectURL(blob);
|
|
351
|
+
const link = document.createElement('a');
|
|
352
|
+
link.href = url;
|
|
353
|
+
link.download = fileName;
|
|
354
|
+
document.body.appendChild(link);
|
|
355
|
+
link.click();
|
|
356
|
+
document.body.removeChild(link);
|
|
357
|
+
window.URL.revokeObjectURL(url);
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
const triggerBrowserUrlDownload = (url, fileName) => {
|
|
361
|
+
const link = document.createElement('a');
|
|
362
|
+
link.href = url;
|
|
363
|
+
link.download = fileName;
|
|
364
|
+
link.target = '_blank';
|
|
365
|
+
link.rel = 'noopener';
|
|
366
|
+
document.body.appendChild(link);
|
|
367
|
+
link.click();
|
|
368
|
+
document.body.removeChild(link);
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
const showDownloadToast = (message, color = 'info') => {
|
|
372
|
+
try {
|
|
373
|
+
const toast = proxy?.$vtk?.message?.toast || window.$vtk?.message?.toast;
|
|
374
|
+
if (typeof toast === 'function') {
|
|
375
|
+
toast(message, { color });
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
} catch (error) {
|
|
379
|
+
console.warn('VtkImg: failed to show message toast', error);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (color === 'error' || color === 'warning') {
|
|
383
|
+
window.alert?.(message);
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
const getDownloadFileHandle = async (fileName, blob) => {
|
|
388
|
+
if (typeof window.showSaveFilePicker !== 'function') {
|
|
389
|
+
showDownloadToast('当前浏览器不支持选择保存目录,将使用默认下载方式。', 'warning');
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const extension = getImageFileExtension(blob);
|
|
394
|
+
const mimeType = imageExtensionMimeMap[extension] || blob?.type || 'image/png';
|
|
395
|
+
|
|
396
|
+
try {
|
|
397
|
+
return await window.showSaveFilePicker({
|
|
398
|
+
suggestedName: fileName,
|
|
399
|
+
types: [
|
|
400
|
+
{
|
|
401
|
+
description: '图片文件',
|
|
402
|
+
accept: {
|
|
403
|
+
[mimeType]: [`.${extension}`],
|
|
404
|
+
},
|
|
405
|
+
},
|
|
406
|
+
],
|
|
407
|
+
});
|
|
408
|
+
} catch (error) {
|
|
409
|
+
if (error?.name === 'AbortError') {
|
|
410
|
+
showDownloadToast('已取消下载。');
|
|
411
|
+
return false;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
console.warn('VtkImg: showSaveFilePicker unavailable, fallback to browser download', error);
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
const isImageResponse = (response, blob, url) => {
|
|
420
|
+
const contentType = response.headers.get('content-type')?.split(';')[0]?.trim().toLowerCase();
|
|
421
|
+
|
|
422
|
+
if (contentType?.startsWith('image/')) return true;
|
|
423
|
+
if (blob.type?.startsWith('image/')) return true;
|
|
424
|
+
|
|
425
|
+
const canFallbackToExtension = !contentType
|
|
426
|
+
|| contentType === 'application/octet-stream'
|
|
427
|
+
|| contentType === 'binary/octet-stream';
|
|
428
|
+
if (!canFallbackToExtension) return false;
|
|
429
|
+
|
|
430
|
+
try {
|
|
431
|
+
const { pathname } = new URL(url, window.location.href);
|
|
432
|
+
const extension = pathname.match(/\.([a-z0-9]+)$/i)?.[1]?.toLowerCase();
|
|
433
|
+
return Boolean(extension && imageExtensionMimeMap[extension]);
|
|
434
|
+
} catch (error) {
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
const fetchImageBlob = async (url) => {
|
|
440
|
+
const response = await fetch(url, { mode: 'cors' });
|
|
441
|
+
if (!response.ok) {
|
|
442
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const blob = await response.blob();
|
|
446
|
+
if (!isImageResponse(response, blob, url)) {
|
|
447
|
+
const contentType = response.headers.get('content-type') || blob.type || 'unknown';
|
|
448
|
+
throw new Error(`Response is not an image: ${contentType}`);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return blob;
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
const fetchCurrentImageBlob = async () => {
|
|
455
|
+
if (!currentImageDownloadUrl.value) {
|
|
456
|
+
throw new Error('Image download url is empty');
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
return fetchImageBlob(currentImageDownloadUrl.value);
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
const downloadCurrentImage = async () => {
|
|
463
|
+
const downloadUrl = currentImageDownloadUrl.value;
|
|
464
|
+
|
|
465
|
+
if (!downloadUrl) {
|
|
466
|
+
showDownloadToast('暂无可下载的图片地址。', 'warning');
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
try {
|
|
471
|
+
const blob = await fetchCurrentImageBlob();
|
|
472
|
+
const fileName = getCurrentImageFileName(blob);
|
|
473
|
+
const fileHandle = await getDownloadFileHandle(fileName, blob);
|
|
474
|
+
if (fileHandle === false) return;
|
|
475
|
+
|
|
476
|
+
if (fileHandle) {
|
|
477
|
+
const writable = await fileHandle.createWritable();
|
|
478
|
+
await writable.write(blob);
|
|
479
|
+
await writable.close();
|
|
480
|
+
showDownloadToast('图片下载成功。', 'success');
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
triggerBrowserDownload(blob, fileName);
|
|
485
|
+
showDownloadToast('图片已开始下载。', 'success');
|
|
486
|
+
} catch (error) {
|
|
487
|
+
console.error('下载图片失败:图片地址不允许跨域读取,请配置资源 CORS 或传入同源 downloadSrc。', error);
|
|
488
|
+
triggerBrowserUrlDownload(downloadUrl, getCurrentImageFileName());
|
|
489
|
+
showDownloadToast('图片下载失败,已切换为浏览器下载。', 'warning');
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
const resetImageTransform = () => {
|
|
494
|
+
imageRotation.value = 0;
|
|
495
|
+
imageScale.value = 1;
|
|
510
496
|
imageTranslateX.value = 0;
|
|
511
497
|
imageTranslateY.value = 0;
|
|
512
498
|
imageDragState.value = null;
|