Переглянути джерело

完成报告截图bug修复

yuanmingze 2 днів тому
батько
коміт
5a9d7a2818
1 змінених файлів з 103 додано та 16 видалено
  1. 103 16
      src/pages-task/task-detail/index.vue

+ 103 - 16
src/pages-task/task-detail/index.vue

@@ -89,6 +89,8 @@ const SHARE_TASK_TYPES = new Set(['8', '9', '10', '11'])
 const SIGN_TASK_TYPES = new Set(['5', '6', '33'])
 const EXPORT_MAX_WAIT_MS = 18_000
 const EXPORT_POLL_INTERVAL_MS = 120
+const EXPORT_STABLE_MS = 300
+const EXPORT_DOCUMENT_CLASS = 'task-detail-export'
 const QR_IMAGE_SELECTOR = '.share-detail-qrcode__image'
 
 const taskId = ref('')
@@ -150,27 +152,61 @@ const getPageRoot = (): Element | null => {
 }
 
 const getQrNaturalWidth = (): number => {
-  const qrImage = getPageRoot()?.querySelector<HTMLImageElement>(QR_IMAGE_SELECTOR)
+  const qrElement = getPageRoot()?.querySelector(QR_IMAGE_SELECTOR)
+  const qrImage = qrElement?.matches('img')
+    ? (qrElement as HTMLImageElement)
+    : qrElement?.querySelector<HTMLImageElement>('img')
   return qrImage ? qrImage.naturalWidth || 0 : -1
 }
 
+const decodedImages = new WeakMap<HTMLImageElement, { src: string; ready: boolean }>()
+
+const isImageDecoded = (image: HTMLImageElement): boolean => {
+  if (!image.complete || image.naturalWidth <= 0) return false
+  if (typeof image.decode !== 'function') return true
+
+  const src = image.currentSrc || image.src
+  let state = decodedImages.get(image)
+  if (!state || state.src !== src) {
+    state = { src, ready: false }
+    decodedImages.set(image, state)
+    const currentState = state
+    void image.decode().then(
+      () => {
+        currentState.ready = true
+      },
+      () => {
+        decodedImages.delete(image)
+      }
+    )
+  }
+  return state.ready
+}
+
 const checkAllImagesOnce = (): ImageReadyState => {
-  const images = Array.from(getPageRoot()?.querySelectorAll<HTMLImageElement>('img') ?? [])
+  const root = getPageRoot()
+  // H5 的 uni-image 在加载成功后才插入 img;容器没有 img 时也必须计入待加载数量。
+  const imageContainers = Array.from(root?.querySelectorAll('uni-image') ?? [])
+  const images = [
+    ...imageContainers.map((container) => container.querySelector<HTMLImageElement>('img')),
+    ...Array.from(root?.querySelectorAll<HTMLImageElement>('img') ?? []).filter(
+      (image) => !image.closest('uni-image')
+    ),
+  ]
   let imgDone = 0
   let imgOk = 0
   let imgErr = 0
 
   for (const image of images) {
-    const isQrImage = image.matches(QR_IMAGE_SELECTOR)
+    if (!image) continue
 
-    if (image.complete && image.naturalWidth > 0) {
+    if (isImageDecoded(image)) {
       imgDone += 1
       imgOk += 1
       continue
     }
 
-    // 二维码必须成功解码;其他图片即使失败也视为加载结束,避免阻塞截图任务。
-    if (!isQrImage && image.complete && image.naturalWidth === 0) {
+    if (image.complete && image.naturalWidth === 0) {
       imgDone += 1
       imgErr += 1
     }
@@ -184,10 +220,10 @@ const checkAllImagesOnce = (): ImageReadyState => {
   }
 }
 
-const mockNotifyExportReady = async (data: UpdateScreenShotStatusParams) => {
+const sendExportReady = async (data: UpdateScreenShotStatusParams) => {
   try {
     const res = await updateScreenShotStatusApi(data)
-    console.log('[task-detail] mockNotifyExportReady', res)
+    console.log('[task-detail] 截图就绪状态通知', data, res)
   } catch (error) {
     console.error('[task-detail] 截图就绪状态通知失败', error)
   }
@@ -210,25 +246,52 @@ const notifyExportReadyOnce = (ok: boolean, timeout: boolean) => {
     ercodeUrl: qrExportState.value.ercodeUrl,
   }
 
-  void mockNotifyExportReady(data)
+  void sendExportReady(data)
 }
 
 const waitAndNotifyExportReady = async (flowVersion: number) => {
   const deadline = exportStartTs + EXPORT_MAX_WAIT_MS
+  let stableSince = 0
+  let previousSnapshot = ''
 
   while (flowVersion === exportFlowVersion && Date.now() < deadline) {
     await waitForPaint()
+    if (flowVersion !== exportFlowVersion) return
 
+    if (errorMessage.value || !taskDetail.value) {
+      notifyExportReadyOnce(false, false)
+      return
+    }
+
+    const root = getPageRoot()
     const imageState = checkAllImagesOnce()
-    const allImagesDone = imageState.imgTotal === 0 || imageState.imgDone >= imageState.imgTotal
+    const allImagesReady = imageState.imgOk === imageState.imgTotal
     const qrReady =
       !shareTaskDetail.value ||
       (Boolean(qrExportState.value.ercodeUrl) &&
-        (qrExportState.value.qrLoadOk || getQrNaturalWidth() > 0))
-
-    if (qrReady && allImagesDone) {
-      notifyExportReadyOnce(true, false)
-      return
+        qrExportState.value.qrLoadOk &&
+        getQrNaturalWidth() > 0)
+
+    if (root && qrReady && allImagesReady && document.fonts?.status !== 'loading') {
+      const snapshot = JSON.stringify({
+        images: Array.from(root.querySelectorAll<HTMLImageElement>('img')).map(
+          (image) => image.currentSrc || image.src
+        ),
+        pageHeight: root.getBoundingClientRect().height,
+        bodyHeight: document.body.getBoundingClientRect().height,
+        scrollHeight: document.documentElement.scrollHeight,
+      })
+
+      if (snapshot !== previousSnapshot) {
+        previousSnapshot = snapshot
+        stableSince = Date.now()
+      } else if (Date.now() - stableSince >= EXPORT_STABLE_MS) {
+        notifyExportReadyOnce(true, false)
+        return
+      }
+    } else {
+      previousSnapshot = ''
+      stableSince = 0
     }
 
     await sleep(EXPORT_POLL_INTERVAL_MS)
@@ -283,8 +346,11 @@ onLoad((options: PageLoadOptions = {}) => {
   exportFlowVersion += 1
   const flowVersion = exportFlowVersion
 
-  taskId.value = String(options.id).trim()
+  taskId.value = String(options.id ?? options.taskId ?? '').trim()
   exportToken.value = String(options.exportToken ?? '').trim()
+  // #ifdef H5
+  document.documentElement.classList.toggle(EXPORT_DOCUMENT_CLASS, Boolean(exportToken.value))
+  // #endif
   exportStartTs = Date.now()
   exportReadyNotified = false
   qrExportState.value = {
@@ -315,9 +381,30 @@ onLoad((options: PageLoadOptions = {}) => {
 
 onUnload(() => {
   exportFlowVersion += 1
+  // #ifdef H5
+  document.documentElement.classList.remove(EXPORT_DOCUMENT_CLASS)
+  // #endif
 })
 </script>
 
+<style>
+/* #ifdef H5 */
+/* 导出时让 body 的实际高度随完整内容增长,覆盖 uni-app 默认的 100% 高度链。 */
+html.task-detail-export,
+html.task-detail-export body,
+html.task-detail-export #app,
+html.task-detail-export uni-app,
+html.task-detail-export uni-page,
+html.task-detail-export uni-page-wrapper,
+html.task-detail-export uni-page-body {
+  height: auto !important;
+  min-height: 100vh;
+  max-height: none !important;
+  overflow: visible !important;
+}
+/* #endif */
+</style>
+
 <style lang="scss" scoped>
 .task-detail-layout {
   min-height: 100vh;