yuanmingze il y a 1 semaine
Parent
commit
1da3a5354e
2 fichiers modifiés avec 162 ajouts et 56 suppressions
  1. 113 47
      src/pages-common/signature/components/SignaturePad.vue
  2. 49 9
      src/utils/loading.ts

+ 113 - 47
src/pages-common/signature/components/SignaturePad.vue

@@ -26,12 +26,13 @@
       class="hidden-canvas"
       id="signature-rotate-canvas"
       canvas-id="signature-rotate-canvas"
+      :style="rotateCanvasStyle"
     />
   </view>
 </template>
 
 <script setup lang="ts">
-import { getCurrentInstance, nextTick, onMounted, ref } from 'vue'
+import { computed, type CSSProperties, getCurrentInstance, nextTick, onMounted, ref } from 'vue'
 
 const CANVAS_ID = 'signature-canvas'
 const ROTATE_CANVAS_ID = 'signature-rotate-canvas'
@@ -47,10 +48,17 @@ const instance = getCurrentInstance()
 const ctx = ref<UniApp.CanvasContext | null>(null)
 const canvasWidth = ref(0)
 const canvasHeight = ref(0)
+const rotateCanvasWidth = ref(1)
+const rotateCanvasHeight = ref(1)
 
 const drawing = ref(false)
 const lastPoint = ref<{ x: number; y: number } | null>(null)
 
+const rotateCanvasStyle = computed<CSSProperties>(() => ({
+  width: `${rotateCanvasWidth.value}px`,
+  height: `${rotateCanvasHeight.value}px`,
+}))
+
 onMounted(async () => {
   await nextTick()
 
@@ -123,59 +131,118 @@ const handleClear = () => {
 }
 
 const handleSubmit = () => {
+  if (!ctx.value) {
+    emit('error', new Error('签名画布未就绪'))
+    return
+  }
+
   uni.showLoading({ title: '生成签名中' })
 
-  uni.canvasToTempFilePath(
-    {
-      canvasId: CANVAS_ID,
-      fileType: 'png',
-      quality: 1,
-      success(res) {
-        rotateImage(res.tempFilePath)
-      },
-      fail(err) {
-        uni.hideLoading()
-        emit('error', err)
+  // 等待最后一笔真正写入视图层后再导出,避免快速提交时拿到空画布。
+  ctx.value.draw(true, () => {
+    uni.canvasToTempFilePath(
+      {
+        canvasId: CANVAS_ID,
+        fileType: 'png',
+        quality: 1,
+        success(res) {
+          rotateImage(res.tempFilePath)
+        },
+        fail(err) {
+          uni.hideLoading()
+          emit('error', err)
+        },
       },
-    },
-    instance?.proxy as any
-  )
+      instance?.proxy as any
+    )
+  })
+}
+
+const waitForRotateCanvasReady = (width: number, height: number): Promise<void> => {
+  return new Promise((resolve, reject) => {
+    const query = uni.createSelectorQuery()
+    if (instance?.proxy) query.in(instance.proxy)
+
+    query
+      .select(`#${ROTATE_CANVAS_ID}`)
+      .boundingClientRect((rect) => {
+        if (
+          !rect ||
+          Array.isArray(rect) ||
+          typeof rect.width !== 'number' ||
+          typeof rect.height !== 'number' ||
+          Math.abs(rect.width - width) > 1 ||
+          Math.abs(rect.height - height) > 1
+        ) {
+          reject(new Error('签名画布尺寸未就绪'))
+          return
+        }
+
+        resolve()
+      })
+      .exec()
+  })
 }
 
 const rotateImage = (src: string) => {
   uni.getImageInfo({
     src,
-    success(info) {
-      const rotateCtx = uni.createCanvasContext(ROTATE_CANVAS_ID, instance?.proxy as any)
-
-      const targetHeight = 300
-      const ratio = info.height / info.width
-      const targetWidth = targetHeight / ratio
-
-      rotateCtx.translate(targetHeight / 2, targetWidth / 2)
-      rotateCtx.rotate((270 * Math.PI) / 180)
-      rotateCtx.drawImage(src, -targetWidth / 2, -targetHeight / 2, targetWidth, targetHeight)
-
-      rotateCtx.draw(false, () => {
-        uni.canvasToTempFilePath(
-          {
-            canvasId: ROTATE_CANVAS_ID,
-            width: targetHeight,
-            height: targetWidth,
-            fileType: 'png',
-            quality: 1,
-            success(res) {
-              uni.hideLoading()
-              emit('submit', res.tempFilePath)
-            },
-            fail(err) {
-              uni.hideLoading()
-              emit('error', err)
-            },
-          },
-          instance?.proxy as any
+    async success(info) {
+      const outputWidth = 300
+      const outputHeight = Math.max(1, Math.round((outputWidth * info.width) / info.height))
+
+      try {
+        rotateCanvasWidth.value = outputWidth
+        rotateCanvasHeight.value = outputHeight
+        await nextTick()
+        await waitForRotateCanvasReady(outputWidth, outputHeight)
+
+        const rotateCtx = uni.createCanvasContext(ROTATE_CANVAS_ID, instance?.proxy as any)
+
+        rotateCtx.setFillStyle('#ffffff')
+        rotateCtx.fillRect(0, 0, outputWidth, outputHeight)
+        rotateCtx.translate(outputWidth / 2, outputHeight / 2)
+        rotateCtx.rotate((270 * Math.PI) / 180)
+        rotateCtx.drawImage(
+          info.path || src,
+          -outputHeight / 2,
+          -outputWidth / 2,
+          outputHeight,
+          outputWidth
         )
-      })
+
+        rotateCtx.draw(false, () => {
+          uni.canvasToTempFilePath(
+            {
+              canvasId: ROTATE_CANVAS_ID,
+              width: outputWidth,
+              height: outputHeight,
+              destWidth: outputWidth,
+              destHeight: outputHeight,
+              fileType: 'png',
+              quality: 1,
+              success(res) {
+                rotateCanvasWidth.value = 1
+                rotateCanvasHeight.value = 1
+                uni.hideLoading()
+                emit('submit', res.tempFilePath)
+              },
+              fail(err) {
+                rotateCanvasWidth.value = 1
+                rotateCanvasHeight.value = 1
+                uni.hideLoading()
+                emit('error', err)
+              },
+            },
+            instance?.proxy as any
+          )
+        })
+      } catch (err) {
+        rotateCanvasWidth.value = 1
+        rotateCanvasHeight.value = 1
+        uni.hideLoading()
+        emit('error', err)
+      }
     },
     fail(err) {
       uni.hideLoading()
@@ -252,8 +319,7 @@ const rotateImage = (src: string) => {
     position: fixed;
     left: -9999px;
     top: -9999px;
-    width: 1px;
-    height: 1px;
+    pointer-events: none;
   }
 }
 </style>

+ 49 - 9
src/utils/loading.ts

@@ -4,6 +4,51 @@ let loadingClosing = false
 let loadingTitle = ''
 let idleWaiters: Array<() => void> = []
 
+interface HideLoadingOptions {
+  fail?: (error: unknown) => void
+}
+
+const ignoreNativePromptFailure = () => undefined
+
+const consumeNativePromptResult = (result: unknown) => {
+  if (
+    result &&
+    typeof result === 'object' &&
+    'catch' in result &&
+    typeof result.catch === 'function'
+  ) {
+    void (result as Promise<unknown>).catch(ignoreNativePromptFailure)
+  }
+}
+
+const showNativeLoading = () => {
+  try {
+    const result = uni.showLoading({
+      title: loadingTitle,
+      mask: true,
+      fail: ignoreNativePromptFailure,
+    })
+
+    consumeNativePromptResult(result)
+  } catch {
+    // Loading 只是辅助 UI,调用失败不应中断业务流程。
+  }
+}
+
+const hideNativeLoading = () => {
+  try {
+    // uni-app 类型声明没有暴露参数,但微信原生 API 支持 fail 回调。
+    // 传入回调可避免 API Promise 化;同时兼容部分端仍返回 Promise 的情况。
+    const result = (uni.hideLoading as unknown as (options: HideLoadingOptions) => unknown)({
+      fail: ignoreNativePromptFailure,
+    })
+
+    consumeNativePromptResult(result)
+  } catch {
+    // 原生层没有 Loading 时可能同步失败,清理操作无需继续抛错。
+  }
+}
+
 const resolveIdleWaiters = () => {
   const waiters = idleWaiters
   idleWaiters = []
@@ -14,14 +59,14 @@ const closeNativeLoading = () => {
   if (!loadingVisible || loadingClosing) return
 
   loadingClosing = true
-  uni.hideLoading()
+  hideNativeLoading()
 
   // 给小程序原生 Loading 留出关闭时间,避免紧随其后的错误弹窗被遮挡。
   setTimeout(() => {
     loadingClosing = false
 
     if (loadingCount > 0) {
-      uni.showLoading({ title: loadingTitle, mask: true })
+      showNativeLoading()
       return
     }
 
@@ -35,10 +80,7 @@ export function showGlobalLoading(title = '') {
 
   if (!loadingVisible) {
     loadingVisible = true
-    uni.showLoading({
-      title,
-      mask: true,
-    })
+    showNativeLoading()
   }
 
   loadingCount++
@@ -75,7 +117,5 @@ export function dismissGlobalLoading(): Promise<void> {
     return waitForGlobalLoading()
   }
 
-  // 兼容页面中尚未迁移到全局工具的 uni.showLoading 调用。
-  uni.hideLoading()
-  return new Promise((resolve) => setTimeout(resolve, 50))
+  return Promise.resolve()
 }