diff --git a/src/composables/useScreenCapture.ts b/src/composables/useScreenCapture.ts index a20a6424..1d17dd75 100644 --- a/src/composables/useScreenCapture.ts +++ b/src/composables/useScreenCapture.ts @@ -4,6 +4,7 @@ */ import { ref } from 'vue' import { captureAsImageData } from '@/utils/snapCapture' +import { waitForCaptureLayoutStabilization } from '@/utils/captureLayout' import { useToast } from '@/composables/useToast' import { useLayoutStore } from '@/stores/layout' import { usePlatformService } from '@/services' @@ -149,7 +150,6 @@ export function useScreenCapture() { } // 移动端宽度适配(渐进式缩放) - let appliedMobileWidth = false if (options?.mobileWidth) { const baseWidth = typeof options.mobileWidth === 'number' ? options.mobileWidth : DEFAULT_MOBILE_MAX_WIDTH @@ -166,7 +166,6 @@ export function useScreenCapture() { element.style.width = `${targetWidth}px` element.style.minWidth = `${targetWidth}px` element.style.maxWidth = `${targetWidth}px` - appliedMobileWidth = true } } @@ -402,14 +401,7 @@ export function useScreenCapture() { } try { - // 如果应用了移动端宽度,等待 DOM 重新布局 - if (appliedMobileWidth) { - await new Promise((resolve) => { - requestAnimationFrame(() => { - requestAnimationFrame(() => resolve()) - }) - }) - } + await waitForCaptureLayoutStabilization() const imageData = await captureAsImageData(element, { maxExportWidth: options?.maxExportWidth, @@ -492,6 +484,7 @@ export function useScreenCapture() { for (const el of hiddenElements) { el.classList.remove('__capture-hidden__') } + await waitForCaptureLayoutStabilization() isCapturing.value = false } } diff --git a/src/utils/captureLayout.test.ts b/src/utils/captureLayout.test.ts new file mode 100644 index 00000000..ef0a41d5 --- /dev/null +++ b/src/utils/captureLayout.test.ts @@ -0,0 +1,20 @@ +import { describe, it } from 'node:test' +import assert from 'node:assert/strict' +import { waitForCaptureLayoutStabilization } from './captureLayout' + +describe('waitForCaptureLayoutStabilization', () => { + it('waits for layout, dispatches resize, then waits for chart repaint', async () => { + const calls: string[] = [] + + await waitForCaptureLayoutStabilization({ + waitFrame: async () => { + calls.push('frame') + }, + dispatchResize: () => { + calls.push('resize') + }, + }) + + assert.deepEqual(calls, ['frame', 'resize', 'frame']) + }) +}) diff --git a/src/utils/captureLayout.ts b/src/utils/captureLayout.ts new file mode 100644 index 00000000..f22f6335 --- /dev/null +++ b/src/utils/captureLayout.ts @@ -0,0 +1,24 @@ +interface CaptureLayoutStabilizationOptions { + waitFrame?: () => Promise + dispatchResize?: () => void +} + +function waitAnimationFrame(): Promise { + return new Promise((resolve) => { + requestAnimationFrame(() => resolve()) + }) +} + +function dispatchWindowResize() { + window.dispatchEvent(new Event('resize')) +} + +export async function waitForCaptureLayoutStabilization(options?: CaptureLayoutStabilizationOptions): Promise { + const waitFrame = options?.waitFrame ?? waitAnimationFrame + const dispatchResize = options?.dispatchResize ?? dispatchWindowResize + + // 截图前会临时修改 padding/overflow,先等一次布局完成,再通知 ECharts 按新容器宽度重绘。 + await waitFrame() + dispatchResize() + await waitFrame() +}