@yxhl/specter-pui-vtk 1.0.90 → 1.0.92

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.90",
3
+ "version": "1.0.92",
4
4
  "description": "雅心互联 Vue 3 + Vuetify3 组件库",
5
5
  "type": "module",
6
6
  "main": "./dist/specter-pui.umd.js",
@@ -1,20 +1,18 @@
1
- <template>
2
- <v-card class="pa-4 mx-auto text-center pt-10" color="transparent" elevation="0" style="border: none !important;box-shadow: none !important;">
3
- <v-img contain width="120" class="mx-auto" src="@/assets/img/empty.svg" alt="" />
4
- <p class="text-grey mt-1">{{ text || "暂无数据" }}</p>
5
- </v-card>
6
- </template>
7
-
8
- <script setup>
9
- import { useAttrs } from 'vue';
10
-
11
- const attrs = useAttrs();
12
-
13
- // 定义 props
14
- const props = defineProps({
15
- text: {
16
- type: String,
17
- default: null,
1
+ <template>
2
+ <v-card class="pa-4 mx-auto text-center pt-10" color="transparent" elevation="0" style="border: none !important;box-shadow: none !important;">
3
+ <v-img contain width="120" class="mx-auto" :src="emptyImg" alt="" />
4
+ <p class="text-grey mt-1">{{ text || "暂无数据" }}</p>
5
+ </v-card>
6
+ </template>
7
+
8
+ <script setup>
9
+ import emptyImg from '@/assets/img/empty.svg';
10
+
11
+ // 定义 props
12
+ defineProps({
13
+ text: {
14
+ type: String,
15
+ default: null,
18
16
  },
19
17
  items: {
20
18
  type: Array,
@@ -23,4 +21,4 @@ const props = defineProps({
23
21
  });
24
22
  </script>
25
23
 
26
- <style lang="scss" scoped></style>
24
+ <style lang="scss" scoped></style>
@@ -326,9 +326,9 @@ const getImageFileExtension = (blob) => {
326
326
  }
327
327
  };
328
328
 
329
- const getCurrentImageFileName = (blob) => {
330
- const extension = getImageFileExtension(blob);
331
- const titleName = currentImageTitle.value?.trim();
329
+ const getCurrentImageFileName = (blob) => {
330
+ const extension = getImageFileExtension(blob);
331
+ const titleName = currentImageTitle.value?.trim();
332
332
 
333
333
  if (titleName) {
334
334
  const safeTitle = titleName.replace(/[\\/:*?"<>|]/g, '_').replace(/\.[a-z0-9]+$/i, '');
@@ -343,19 +343,86 @@ const getCurrentImageFileName = (blob) => {
343
343
  return urlName ? `${urlName}.${extension}` : `image.${extension}`;
344
344
  } catch (error) {
345
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
- };
346
+ }
347
+ };
348
+
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);
354
+ };
355
+
356
+ const getDownloadFailureReason = (error) => {
357
+ const message = error?.message || '';
358
+ const name = error?.name || '';
359
+
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 '图片地址不允许跨域读取,或当前网络无法访问图片';
365
+ }
366
+ if (/HTTP error! status:\s*(\d+)/i.test(message)) {
367
+ return `图片请求失败,状态码 ${message.match(/HTTP error! status:\s*(\d+)/i)?.[1]}`;
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 || '未知错误';
375
+ };
376
+
377
+ const validateImageBlob = async (blob) => {
378
+ if (!blob || blob.size <= 0) {
379
+ throw new Error('Downloaded image is empty');
380
+ }
381
+
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}`);
386
+ }
387
+
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
+ }
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
+ });
414
+ };
415
+
416
+ const triggerBrowserDownload = (blob, fileName) => {
417
+ const url = window.URL.createObjectURL(blob);
418
+ const link = document.createElement('a');
419
+ link.href = url;
420
+ link.download = fileName;
421
+ document.body.appendChild(link);
422
+ link.click();
423
+ document.body.removeChild(link);
424
+ setTimeout(() => window.URL.revokeObjectURL(url), 60 * 1000);
425
+ };
359
426
 
360
427
  const triggerBrowserUrlDownload = (url, fileName) => {
361
428
  const link = document.createElement('a');
@@ -384,11 +451,15 @@ const showDownloadToast = (message, color = 'info') => {
384
451
  }
385
452
  };
386
453
 
387
- const getDownloadFileHandle = async (fileName, blob) => {
388
- if (typeof window.showSaveFilePicker !== 'function') {
389
- showDownloadToast('当前浏览器不支持选择保存目录,将使用默认下载方式。', 'warning');
390
- return null;
391
- }
454
+ const getDownloadFileHandle = async (fileName, blob) => {
455
+ if (isDownloadRiskWebView()) {
456
+ return null;
457
+ }
458
+
459
+ if (typeof window.showSaveFilePicker !== 'function') {
460
+ showDownloadToast('当前浏览器不支持选择保存目录,将使用默认下载方式。', 'warning');
461
+ return null;
462
+ }
392
463
 
393
464
  const extension = getImageFileExtension(blob);
394
465
  const mimeType = imageExtensionMimeMap[extension] || blob?.type || 'image/png';
@@ -467,11 +538,12 @@ const downloadCurrentImage = async () => {
467
538
  return;
468
539
  }
469
540
 
470
- try {
471
- const blob = await fetchCurrentImageBlob();
472
- const fileName = getCurrentImageFileName(blob);
473
- const fileHandle = await getDownloadFileHandle(fileName, blob);
474
- if (fileHandle === false) return;
541
+ try {
542
+ const blob = await fetchCurrentImageBlob();
543
+ await validateImageBlob(blob);
544
+ const fileName = getCurrentImageFileName(blob);
545
+ const fileHandle = await getDownloadFileHandle(fileName, blob);
546
+ if (fileHandle === false) return;
475
547
 
476
548
  if (fileHandle) {
477
549
  const writable = await fileHandle.createWritable();
@@ -479,16 +551,27 @@ const downloadCurrentImage = async () => {
479
551
  await writable.close();
480
552
  showDownloadToast('图片下载成功。', 'success');
481
553
  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
- };
554
+ }
555
+
556
+ triggerBrowserDownload(blob, fileName);
557
+ if (isDownloadRiskWebView()) {
558
+ showDownloadToast('已使用兼容方式下载图片,如文件仍不可用,请在系统浏览器中打开后保存。');
559
+ return;
560
+ }
561
+
562
+ showDownloadToast('图片已开始下载。', 'success');
563
+ } catch (error) {
564
+ const failureReason = getDownloadFailureReason(error);
565
+ console.error(`下载图片失败:${failureReason}`, error);
566
+ triggerBrowserUrlDownload(downloadUrl, getCurrentImageFileName());
567
+ if (isDownloadRiskWebView()) {
568
+ showDownloadToast(`下载失败:${failureReason}。已打开原图,请在系统浏览器中保存。`, 'warning');
569
+ return;
570
+ }
571
+
572
+ showDownloadToast(`下载失败:${failureReason}。已切换为浏览器下载。`, 'warning');
573
+ }
574
+ };
492
575
 
493
576
  const resetImageTransform = () => {
494
577
  imageRotation.value = 0;
package/src/index.js CHANGED
@@ -11,9 +11,10 @@ export { default as VtkDatePicker } from "./components/assembly/VtkDatePicker.vu
11
11
  export { default as VtkDateSelector } from "./components/assembly/VtkDateSelector.vue";
12
12
  export { default as VtkDateTimePicker } from "./components/assembly/VtkDateTimePicker.vue";
13
13
  export { default as VtkDept } from "./components/assembly/VtkDept.vue";
14
- export { default as VtkEmpty } from "./components/assembly/VtkEmpty.vue";
15
- export { default as VtkFab } from "./components/assembly/VtkFab.vue";
16
- export { default as VtkFormItem } from "./components/assembly/VtkFormItem.vue";
14
+ export { default as VtkEmpty } from "./components/assembly/VtkEmpty.vue";
15
+ export { default as VtkEmptyNew } from "./components/assembly/VtkEmptyNew.vue";
16
+ export { default as VtkFab } from "./components/assembly/VtkFab.vue";
17
+ export { default as VtkFormItem } from "./components/assembly/VtkFormItem.vue";
17
18
  export { default as VtkImg } from "./components/assembly/VtkImg.vue";
18
19
  export { default as VtkPage } from "./components/assembly/VtkPage.vue";
19
20
  export { default as VtkPdf } from "./components/assembly/VtkPdf.vue";
@@ -55,9 +56,10 @@ import VtkDatePicker from "./components/assembly/VtkDatePicker.vue";
55
56
  import VtkDateSelector from "./components/assembly/VtkDateSelector.vue";
56
57
  import VtkDateTimePicker from "./components/assembly/VtkDateTimePicker.vue";
57
58
  import VtkDept from "./components/assembly/VtkDept.vue";
58
- import VtkEmpty from "./components/assembly/VtkEmpty.vue";
59
- import VtkFab from "./components/assembly/VtkFab.vue";
60
- import VtkFormItem from "./components/assembly/VtkFormItem.vue";
59
+ import VtkEmpty from "./components/assembly/VtkEmpty.vue";
60
+ import VtkEmptyNew from "./components/assembly/VtkEmptyNew.vue";
61
+ import VtkFab from "./components/assembly/VtkFab.vue";
62
+ import VtkFormItem from "./components/assembly/VtkFormItem.vue";
61
63
  import VtkImg from "./components/assembly/VtkImg.vue";
62
64
  import VtkPage from "./components/assembly/VtkPage.vue";
63
65
  import VtkPdf from "./components/assembly/VtkPdf.vue";
@@ -80,9 +82,10 @@ function install(app, options = {}) {
80
82
  VtkDateSelector,
81
83
  VtkDateTimePicker,
82
84
  VtkDept,
83
- VtkEmpty,
84
- VtkFab,
85
- VtkFormItem,
85
+ VtkEmpty,
86
+ VtkEmptyNew,
87
+ VtkFab,
88
+ VtkFormItem,
86
89
  VtkImg,
87
90
  VtkPage,
88
91
  VtkPdf,
@@ -128,8 +131,9 @@ export default {
128
131
  VtkDateSelector,
129
132
  VtkDateTimePicker,
130
133
  VtkDept,
131
- VtkEmpty,
132
- VtkFab,
134
+ VtkEmpty,
135
+ VtkEmptyNew,
136
+ VtkFab,
133
137
  VtkFormItem,
134
138
  VtkImg,
135
139
  VtkPage,