|
|
@@ -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;
|