@yxhl/specter-pui-vtk 1.0.89 → 1.0.91
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 +2344 -2310
- 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 +301 -232
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,65 +285,132 @@ 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',
|
|
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
|
+
}
|
|
301
347
|
};
|
|
302
348
|
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
png: 'image/png',
|
|
309
|
-
svg: 'image/svg+xml',
|
|
310
|
-
webp: 'image/webp',
|
|
349
|
+
const isDownloadRiskWebView = () => {
|
|
350
|
+
const ua = window.navigator?.userAgent || '';
|
|
351
|
+
const hasEmbeddedBridge = typeof window.dd !== 'undefined' || typeof window.AlipayJSBridge !== 'undefined';
|
|
352
|
+
return hasEmbeddedBridge
|
|
353
|
+
|| /DingTalk|AliApp\(DingTalk|zjzwfw|zzd|ZJGovernment|ZhejiangGovernment|GovDing|Electron|WebView|; wv\)/i.test(ua);
|
|
311
354
|
};
|
|
312
355
|
|
|
313
|
-
const
|
|
314
|
-
const
|
|
356
|
+
const getDownloadFailureReason = (error) => {
|
|
357
|
+
const message = error?.message || '';
|
|
358
|
+
const name = error?.name || '';
|
|
315
359
|
|
|
316
|
-
if (
|
|
317
|
-
|
|
360
|
+
if (name === 'AbortError') return '已取消下载';
|
|
361
|
+
if (name === 'NotAllowedError' || name === 'SecurityError') return '当前客户端阻止了文件保存';
|
|
362
|
+
if (name === 'QuotaExceededError') return '存储空间不足或客户端限制写入';
|
|
363
|
+
if (/Failed to fetch|NetworkError|Load failed/i.test(message)) {
|
|
364
|
+
return '图片地址不允许跨域读取,或当前网络无法访问图片';
|
|
318
365
|
}
|
|
319
|
-
|
|
320
|
-
|
|
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';
|
|
366
|
+
if (/HTTP error! status:\s*(\d+)/i.test(message)) {
|
|
367
|
+
return `图片请求失败,状态码 ${message.match(/HTTP error! status:\s*(\d+)/i)?.[1]}`;
|
|
326
368
|
}
|
|
369
|
+
if (/Response is not an image/i.test(message)) return '下载地址返回的不是图片文件';
|
|
370
|
+
if (/Downloaded image is empty/i.test(message)) return '下载到的图片文件为空';
|
|
371
|
+
if (/Downloaded file is not an image/i.test(message)) return '下载到的文件不是图片格式';
|
|
372
|
+
if (/Downloaded image cannot be decoded/i.test(message)) return '下载到的图片无法被浏览器解析';
|
|
373
|
+
|
|
374
|
+
return message || '未知错误';
|
|
327
375
|
};
|
|
328
376
|
|
|
329
|
-
const
|
|
330
|
-
|
|
331
|
-
|
|
377
|
+
const validateImageBlob = async (blob) => {
|
|
378
|
+
if (!blob || blob.size <= 0) {
|
|
379
|
+
throw new Error('Downloaded image is empty');
|
|
380
|
+
}
|
|
332
381
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
382
|
+
const extension = getImageFileExtension(blob);
|
|
383
|
+
const mimeType = blob.type?.split(';')[0]?.trim().toLowerCase();
|
|
384
|
+
if (mimeType && !mimeType.startsWith('image/')) {
|
|
385
|
+
throw new Error(`Downloaded file is not an image: ${mimeType}`);
|
|
336
386
|
}
|
|
337
387
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
.
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
388
|
+
if (typeof window.createImageBitmap === 'function' && mimeType !== 'image/svg+xml') {
|
|
389
|
+
try {
|
|
390
|
+
const bitmap = await window.createImageBitmap(blob);
|
|
391
|
+
bitmap.close?.();
|
|
392
|
+
return;
|
|
393
|
+
} catch (error) {
|
|
394
|
+
console.warn('VtkImg: createImageBitmap validation failed, fallback to Image validation', error);
|
|
395
|
+
}
|
|
346
396
|
}
|
|
397
|
+
|
|
398
|
+
if (extension === 'svg') return;
|
|
399
|
+
|
|
400
|
+
await new Promise((resolve, reject) => {
|
|
401
|
+
const url = window.URL.createObjectURL(blob);
|
|
402
|
+
const image = new Image();
|
|
403
|
+
|
|
404
|
+
image.onload = () => {
|
|
405
|
+
window.URL.revokeObjectURL(url);
|
|
406
|
+
resolve();
|
|
407
|
+
};
|
|
408
|
+
image.onerror = () => {
|
|
409
|
+
window.URL.revokeObjectURL(url);
|
|
410
|
+
reject(new Error('Downloaded image cannot be decoded'));
|
|
411
|
+
};
|
|
412
|
+
image.src = url;
|
|
413
|
+
});
|
|
347
414
|
};
|
|
348
415
|
|
|
349
416
|
const triggerBrowserDownload = (blob, fileName) => {
|
|
@@ -354,159 +421,161 @@ const triggerBrowserDownload = (blob, fileName) => {
|
|
|
354
421
|
document.body.appendChild(link);
|
|
355
422
|
link.click();
|
|
356
423
|
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 isCrossOriginUrl = (url) => {
|
|
388
|
-
try {
|
|
389
|
-
return new URL(url, window.location.href).origin !== window.location.origin;
|
|
390
|
-
} catch (error) {
|
|
391
|
-
return false;
|
|
392
|
-
}
|
|
424
|
+
setTimeout(() => window.URL.revokeObjectURL(url), 60 * 1000);
|
|
393
425
|
};
|
|
394
|
-
|
|
426
|
+
|
|
427
|
+
const triggerBrowserUrlDownload = (url, fileName) => {
|
|
428
|
+
const link = document.createElement('a');
|
|
429
|
+
link.href = url;
|
|
430
|
+
link.download = fileName;
|
|
431
|
+
link.target = '_blank';
|
|
432
|
+
link.rel = 'noopener';
|
|
433
|
+
document.body.appendChild(link);
|
|
434
|
+
link.click();
|
|
435
|
+
document.body.removeChild(link);
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
const showDownloadToast = (message, color = 'info') => {
|
|
439
|
+
try {
|
|
440
|
+
const toast = proxy?.$vtk?.message?.toast || window.$vtk?.message?.toast;
|
|
441
|
+
if (typeof toast === 'function') {
|
|
442
|
+
toast(message, { color });
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
} catch (error) {
|
|
446
|
+
console.warn('VtkImg: failed to show message toast', error);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (color === 'error' || color === 'warning') {
|
|
450
|
+
window.alert?.(message);
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
|
|
395
454
|
const getDownloadFileHandle = async (fileName, blob) => {
|
|
396
|
-
if (
|
|
397
|
-
showDownloadToast('当前浏览器不支持选择保存目录,将使用默认下载方式。', 'warning');
|
|
455
|
+
if (isDownloadRiskWebView()) {
|
|
398
456
|
return null;
|
|
399
457
|
}
|
|
400
458
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
try {
|
|
405
|
-
return await window.showSaveFilePicker({
|
|
406
|
-
suggestedName: fileName,
|
|
407
|
-
types: [
|
|
408
|
-
{
|
|
409
|
-
description: '图片文件',
|
|
410
|
-
accept: {
|
|
411
|
-
[mimeType]: [`.${extension}`],
|
|
412
|
-
},
|
|
413
|
-
},
|
|
414
|
-
],
|
|
415
|
-
});
|
|
416
|
-
} catch (error) {
|
|
417
|
-
if (error?.name === 'AbortError') {
|
|
418
|
-
showDownloadToast('已取消下载。');
|
|
419
|
-
return false;
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
console.warn('VtkImg: showSaveFilePicker unavailable, fallback to browser download', error);
|
|
459
|
+
if (typeof window.showSaveFilePicker !== 'function') {
|
|
460
|
+
showDownloadToast('当前浏览器不支持选择保存目录,将使用默认下载方式。', 'warning');
|
|
423
461
|
return null;
|
|
424
462
|
}
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
const
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
const
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
}
|
|
483
|
-
|
|
463
|
+
|
|
464
|
+
const extension = getImageFileExtension(blob);
|
|
465
|
+
const mimeType = imageExtensionMimeMap[extension] || blob?.type || 'image/png';
|
|
466
|
+
|
|
467
|
+
try {
|
|
468
|
+
return await window.showSaveFilePicker({
|
|
469
|
+
suggestedName: fileName,
|
|
470
|
+
types: [
|
|
471
|
+
{
|
|
472
|
+
description: '图片文件',
|
|
473
|
+
accept: {
|
|
474
|
+
[mimeType]: [`.${extension}`],
|
|
475
|
+
},
|
|
476
|
+
},
|
|
477
|
+
],
|
|
478
|
+
});
|
|
479
|
+
} catch (error) {
|
|
480
|
+
if (error?.name === 'AbortError') {
|
|
481
|
+
showDownloadToast('已取消下载。');
|
|
482
|
+
return false;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
console.warn('VtkImg: showSaveFilePicker unavailable, fallback to browser download', error);
|
|
486
|
+
return null;
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
const isImageResponse = (response, blob, url) => {
|
|
491
|
+
const contentType = response.headers.get('content-type')?.split(';')[0]?.trim().toLowerCase();
|
|
492
|
+
|
|
493
|
+
if (contentType?.startsWith('image/')) return true;
|
|
494
|
+
if (blob.type?.startsWith('image/')) return true;
|
|
495
|
+
|
|
496
|
+
const canFallbackToExtension = !contentType
|
|
497
|
+
|| contentType === 'application/octet-stream'
|
|
498
|
+
|| contentType === 'binary/octet-stream';
|
|
499
|
+
if (!canFallbackToExtension) return false;
|
|
500
|
+
|
|
501
|
+
try {
|
|
502
|
+
const { pathname } = new URL(url, window.location.href);
|
|
503
|
+
const extension = pathname.match(/\.([a-z0-9]+)$/i)?.[1]?.toLowerCase();
|
|
504
|
+
return Boolean(extension && imageExtensionMimeMap[extension]);
|
|
505
|
+
} catch (error) {
|
|
506
|
+
return false;
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
const fetchImageBlob = async (url) => {
|
|
511
|
+
const response = await fetch(url, { mode: 'cors' });
|
|
512
|
+
if (!response.ok) {
|
|
513
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const blob = await response.blob();
|
|
517
|
+
if (!isImageResponse(response, blob, url)) {
|
|
518
|
+
const contentType = response.headers.get('content-type') || blob.type || 'unknown';
|
|
519
|
+
throw new Error(`Response is not an image: ${contentType}`);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
return blob;
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
const fetchCurrentImageBlob = async () => {
|
|
526
|
+
if (!currentImageDownloadUrl.value) {
|
|
527
|
+
throw new Error('Image download url is empty');
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
return fetchImageBlob(currentImageDownloadUrl.value);
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
const downloadCurrentImage = async () => {
|
|
534
|
+
const downloadUrl = currentImageDownloadUrl.value;
|
|
535
|
+
|
|
536
|
+
if (!downloadUrl) {
|
|
537
|
+
showDownloadToast('暂无可下载的图片地址。', 'warning');
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
|
|
484
541
|
try {
|
|
485
542
|
const blob = await fetchCurrentImageBlob();
|
|
543
|
+
await validateImageBlob(blob);
|
|
486
544
|
const fileName = getCurrentImageFileName(blob);
|
|
487
545
|
const fileHandle = await getDownloadFileHandle(fileName, blob);
|
|
488
546
|
if (fileHandle === false) return;
|
|
547
|
+
|
|
548
|
+
if (fileHandle) {
|
|
549
|
+
const writable = await fileHandle.createWritable();
|
|
550
|
+
await writable.write(blob);
|
|
551
|
+
await writable.close();
|
|
552
|
+
showDownloadToast('图片下载成功。', 'success');
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
489
555
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
await writable.close();
|
|
494
|
-
showDownloadToast('图片下载成功。', 'success');
|
|
556
|
+
triggerBrowserDownload(blob, fileName);
|
|
557
|
+
if (isDownloadRiskWebView()) {
|
|
558
|
+
showDownloadToast('已使用兼容方式下载图片,如文件仍不可用,请在系统浏览器中打开后保存。');
|
|
495
559
|
return;
|
|
496
560
|
}
|
|
497
561
|
|
|
498
|
-
triggerBrowserDownload(blob, fileName);
|
|
499
562
|
showDownloadToast('图片已开始下载。', 'success');
|
|
500
563
|
} catch (error) {
|
|
501
|
-
|
|
564
|
+
const failureReason = getDownloadFailureReason(error);
|
|
565
|
+
console.error(`下载图片失败:${failureReason}`, error);
|
|
502
566
|
triggerBrowserUrlDownload(downloadUrl, getCurrentImageFileName());
|
|
503
|
-
|
|
567
|
+
if (isDownloadRiskWebView()) {
|
|
568
|
+
showDownloadToast(`下载失败:${failureReason}。已打开原图,请在系统浏览器中保存。`, 'warning');
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
showDownloadToast(`下载失败:${failureReason}。已切换为浏览器下载。`, 'warning');
|
|
504
573
|
}
|
|
505
574
|
};
|
|
506
|
-
|
|
507
|
-
const resetImageTransform = () => {
|
|
508
|
-
imageRotation.value = 0;
|
|
509
|
-
imageScale.value = 1;
|
|
575
|
+
|
|
576
|
+
const resetImageTransform = () => {
|
|
577
|
+
imageRotation.value = 0;
|
|
578
|
+
imageScale.value = 1;
|
|
510
579
|
imageTranslateX.value = 0;
|
|
511
580
|
imageTranslateY.value = 0;
|
|
512
581
|
imageDragState.value = null;
|