/** * 增强版截图服务 * 结合多种技术方案确保能够成功捕获页面截图 */ // 动态导入所需库 let htmlToImageModule: any = null let domToImageModule: any = null // 加载所需库 async function loadScreenshotLibraries() { if (!htmlToImageModule) { try { htmlToImageModule = await import("html-to-image") console.log("html-to-image库加载成功") } catch (error) { console.warn("html-to-image库加载失败:", error) } } if (!domToImageModule) { try { domToImageModule = await import("dom-to-image") console.log("dom-to-image库加载成功") } catch (error) { console.warn("dom-to-image库加载失败:", error) } } return { htmlToImage: htmlToImageModule, domToImage: domToImageModule, } } /** * 使用多种方法尝试捕获页面截图 * @param targetElement 目标元素(iframe或窗口) * @param url 页面URL * @param options 配置选项 * @returns 截图的base64数据URL */ export async function captureScreenshot( targetElement: HTMLIFrameElement | Window, url: string, options: { maxRetries?: number retryDelay?: number useDirectCapture?: boolean useWindowOpen?: boolean } = {}, ): Promise { const { maxRetries = 3, retryDelay = 1000, useDirectCapture = true, useWindowOpen = false } = options let lastError: any = null // 尝试多种方法捕获截图 for (let retry = 0; retry < maxRetries; retry++) { try { // 如果重试,等待一段时间 if (retry > 0) { await new Promise((resolve) => setTimeout(resolve, retryDelay)) console.log(`尝试第 ${retry + 1} 次捕获截图...`) } // 方法1: 使用html-to-image或dom-to-image库 if (useDirectCapture) { const libraries = await loadScreenshotLibraries() // 确定目标元素 let element: HTMLElement | null = null if (targetElement instanceof HTMLIFrameElement) { try { // 尝试访问iframe内容 const iframeDocument = targetElement.contentDocument || targetElement.contentWindow?.document if (iframeDocument) { element = iframeDocument.documentElement } } catch (error) { console.warn("无法访问iframe内容,可能是跨域限制:", error) } } else if (targetElement instanceof Window) { element = targetElement.document.documentElement } if (element) { // 尝试使用html-to-image if (libraries.htmlToImage) { try { console.log("使用html-to-image捕获截图...") const dataUrl = await libraries.htmlToImage.toPng(element, { quality: 0.95, cacheBust: true, pixelRatio: 2, skipAutoScale: true, style: { "background-color": "#ffffff", }, }) if (dataUrl && dataUrl.startsWith("data:image/png;base64,")) { console.log("html-to-image捕获成功!") return dataUrl } } catch (error) { console.warn("html-to-image捕获失败:", error) } } // 尝试使用dom-to-image if (libraries.domToImage) { try { console.log("使用dom-to-image捕获截图...") const dataUrl = await libraries.domToImage.toPng(element, { quality: 0.95, bgcolor: "#ffffff", scale: 2, }) if (dataUrl && dataUrl.startsWith("data:image/png;base64,")) { console.log("dom-to-image捕获成功!") return dataUrl } } catch (error) { console.warn("dom-to-image捕获失败:", error) } } } } // 方法2: 使用Canvas API try { console.log("使用Canvas API捕获截图...") const screenshot = await captureWithCanvas(targetElement) if (screenshot) { console.log("Canvas API捕获成功!") return screenshot } } catch (error) { console.warn("Canvas API捕获失败:", error) lastError = error } // 方法3: 使用postMessage通信 if (targetElement instanceof HTMLIFrameElement) { try { console.log("使用postMessage尝试捕获截图...") const screenshot = await captureWithPostMessage(targetElement) if (screenshot) { console.log("postMessage捕获成功!") return screenshot } } catch (error) { console.warn("postMessage捕获失败:", error) lastError = error } } // 方法4: 使用window.open打开页面 if (useWindowOpen) { try { console.log("使用window.open尝试捕获截图...") const screenshot = await captureWithWindowOpen(url) if (screenshot) { console.log("window.open捕获成功!") return screenshot } } catch (error) { console.warn("window.open捕获失败:", error) lastError = error } } } catch (error) { console.error(`第 ${retry + 1} 次捕获截图失败:`, error) lastError = error } } // 所有方法都失败,使用备用方案 console.warn("所有截图方法都失败,使用备用方案") return createFallbackScreenshot(url, lastError) } /** * 使用Canvas API捕获截图 */ async function captureWithCanvas(target: HTMLIFrameElement | Window): Promise { try { let document: Document | null = null let width = 1200 let height = 800 if (target instanceof HTMLIFrameElement) { if (!target.contentDocument && !target.contentWindow?.document) { throw new Error("无法访问iframe内容") } document = target.contentDocument || target.contentWindow?.document || null width = target.offsetWidth || width height = target.offsetHeight || height } else { document = target.document width = target.innerWidth height = target.innerHeight } if (!document) { throw new Error("无法获取文档对象") } // 创建canvas const canvas = document.createElement("canvas") canvas.width = width canvas.height = height const ctx = canvas.getContext("2d") if (!ctx) { throw new Error("无法创建canvas上下文") } // 绘制白色背景 ctx.fillStyle = "#ffffff" ctx.fillRect(0, 0, width, height) // 创建一个图像对象 const img = new Image() img.crossOrigin = "anonymous" // 将文档转换为SVG数据URL const data = new XMLSerializer().serializeToString(document.documentElement) const svg = new Blob([data], { type: "image/svg+xml" }) const url = URL.createObjectURL(svg) // 等待图像加载 await new Promise((resolve, reject) => { img.onload = resolve img.onerror = reject img.src = url }) // 绘制图像 ctx.drawImage(img, 0, 0, width, height) URL.revokeObjectURL(url) // 返回数据URL return canvas.toDataURL("image/png") } catch (error) { console.error("Canvas捕获失败:", error) return null } } /** * 使用postMessage与iframe通信捕获截图 */ async function captureWithPostMessage(iframe: HTMLIFrameElement): Promise { return new Promise((resolve, reject) => { // 设置超时 const timeout = setTimeout(() => { window.removeEventListener("message", messageHandler) reject(new Error("postMessage截图超时")) }, 5000) // 消息处理函数 function messageHandler(event: MessageEvent) { if (event.data && event.data.type === "screenshot" && event.data.dataUrl) { clearTimeout(timeout) window.removeEventListener("message", messageHandler) resolve(event.data.dataUrl) } } // 监听消息 window.addEventListener("message", messageHandler) // 向iframe发送消息 try { iframe.contentWindow?.postMessage({ type: "requestScreenshot" }, "*") } catch (error) { clearTimeout(timeout) window.removeEventListener("message", messageHandler) reject(error) } }) } /** * 使用window.open打开页面并捕获截图 */ async function captureWithWindowOpen(url: string): Promise { return new Promise((resolve, reject) => { try { // 打开新窗口 const newWindow = window.open(url, "_blank", "width=1200,height=800") if (!newWindow) { reject(new Error("无法打开新窗口,可能被浏览器阻止")) return } // 设置超时 const timeout = setTimeout(() => { if (newWindow) newWindow.close() reject(new Error("window.open截图超时")) }, 10000) // 等待页面加载完成 newWindow.onload = async () => { try { // 等待一段时间,确保页面完全渲染 await new Promise((resolve) => setTimeout(resolve, 2000)) // 尝试捕获截图 const libraries = await loadScreenshotLibraries() let screenshot = null if (libraries.htmlToImage) { try { screenshot = await libraries.htmlToImage.toPng(newWindow.document.documentElement) } catch (error) { console.warn("html-to-image捕获失败:", error) } } if (!screenshot && libraries.domToImage) { try { screenshot = await libraries.domToImage.toPng(newWindow.document.documentElement) } catch (error) { console.warn("dom-to-image捕获失败:", error) } } // 关闭窗口 newWindow.close() clearTimeout(timeout) if (screenshot) { resolve(screenshot) } else { reject(new Error("无法捕获新窗口的截图")) } } catch (error) { newWindow.close() clearTimeout(timeout) reject(error) } } // 处理加载错误 newWindow.onerror = (event) => { newWindow.close() clearTimeout(timeout) reject(new Error("新窗口加载出错")) } } catch (error) { reject(error) } }) } /** * 创建备用截图 * 当所有方法都失败时,生成一个包含页面信息的模拟截图 */ function createFallbackScreenshot(url: string, error: any): string { // 提取页面标题 const pathParts = url.split("/") const pageName = pathParts[pathParts.length - 1] || "页面" // 创建canvas const canvas = document.createElement("canvas") const ctx = canvas.getContext("2d") if (!ctx) { throw new Error("无法创建canvas上下文") } // 设置canvas尺寸 const width = 1200 const height = 800 canvas.width = width canvas.height = height // 绘制背景 ctx.fillStyle = "#ffffff" ctx.fillRect(0, 0, width, height) // 绘制边框 ctx.strokeStyle = "#e5e7eb" ctx.lineWidth = 2 ctx.strokeRect(1, 1, width - 2, height - 2) // 绘制页面信息 ctx.fillStyle = "#f9fafb" ctx.fillRect(0, 0, width, 80) ctx.fillStyle = "#111827" ctx.font = "bold 24px system-ui" ctx.fillText(pageName, 20, 40) ctx.fillStyle = "#6b7280" ctx.font = "14px system-ui" ctx.fillText(url, 20, 65) // 绘制内容区域 ctx.fillStyle = "#f3f4f6" ctx.fillRect(20, 100, width - 40, height - 120) // 添加错误信息 ctx.fillStyle = "#ef4444" ctx.font = "16px system-ui" ctx.fillText("截图捕获失败", 40, 140) ctx.fillStyle = "#6b7280" ctx.font = "14px system-ui" ctx.fillText(`错误信息: ${error ? error.message || "未知错误" : "未知错误"}`, 40, 170) ctx.fillText(`URL: ${url}`, 40, 200) ctx.fillText("请尝试手动截图或刷新页面后重试", 40, 230) // 添加水印 ctx.save() ctx.globalAlpha = 0.1 ctx.fillStyle = "#000000" ctx.font = "bold 48px system-ui" ctx.translate(width / 2, height / 2) ctx.rotate(-Math.PI / 6) ctx.textAlign = "center" ctx.fillText("备用截图", 0, 0) ctx.restore() // 返回base64数据 return canvas.toDataURL("image/png") } /** * 注入截图脚本到页面 * 这个函数会向页面注入一个脚本,使页面能够响应截图请求 */ export function injectScreenshotScript(window: Window): void { try { const script = window.document.createElement("script") script.textContent = ` // 监听截图请求 window.addEventListener('message', async function(event) { if (event.data && event.data.type === 'requestScreenshot') { try { // 动态加载html2canvas const script = document.createElement('script'); script.src = 'https://html2canvas.hertzen.com/dist/html2canvas.min.js'; script.onload = async function() { try { // 使用html2canvas捕获页面 const canvas = await html2canvas(document.documentElement, { allowTaint: true, useCORS: true, scale: 2, backgroundColor: '#ffffff' }); // 发送截图数据 const dataUrl = canvas.toDataURL('image/png'); window.parent.postMessage({ type: 'screenshot', dataUrl: dataUrl }, '*'); } catch (error) { console.error('html2canvas捕获失败:', error); window.parent.postMessage({ type: 'screenshotError', error: error.toString() }, '*'); } }; script.onerror = function(error) { console.error('加载html2canvas失败:', error); window.parent.postMessage({ type: 'screenshotError', error: 'Failed to load html2canvas' }, '*'); }; document.head.appendChild(script); } catch (error) { console.error('处理截图请求时出错:', error); window.parent.postMessage({ type: 'screenshotError', error: error.toString() }, '*'); } } }); console.log('截图脚本已注入'); ` window.document.head.appendChild(script) } catch (error) { console.error("注入截图脚本失败:", error) } } /** * 使用服务器端API捕获截图 * 注意:这需要后端支持 */ export async function captureWithServerAPI(url: string): Promise { try { // 这里应该调用后端API // 由于我们没有实际的后端API,这里只是一个示例 console.log("尝试使用服务器端API捕获截图:", url) // 模拟API调用 const response = await fetch("/api/screenshot", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ url }), }) if (!response.ok) { throw new Error(`API请求失败: ${response.status} ${response.statusText}`) } const data = await response.json() if (data.success && data.screenshot) { return data.screenshot } else { throw new Error(data.error || "未知错误") } } catch (error) { console.error("服务器端API捕获失败:", error) return null } }