import html2canvas from "html2canvas" import { toPng, toJpeg, toBlob, toSvg } from "html-to-image" export interface ScreenshotOptions { format?: "png" | "jpeg" | "webp" | "svg" quality?: number width?: number height?: number scale?: number backgroundColor?: string skipFonts?: boolean preferredFontFormat?: string timeout?: number } export interface ScreenshotResult { success: boolean dataUrl?: string blob?: Blob error?: string method?: string } class EnhancedScreenshotService { private retryCount = 3 private retryDelay = 1000 async captureElement(element: HTMLElement, options: ScreenshotOptions = {}): Promise { const methods = [ () => this.captureWithHtmlToImage(element, options), () => this.captureWithHtml2Canvas(element, options), () => this.captureWithCanvas(element, options), () => this.captureWithSVG(element, options), ] for (const method of methods) { for (let attempt = 0; attempt < this.retryCount; attempt++) { try { const result = await method() if (result.success) { return result } } catch (error) { console.warn(`截图方法失败 (尝试 ${attempt + 1}):`, error) if (attempt < this.retryCount - 1) { await this.delay(this.retryDelay) } } } } return { success: false, error: "所有截图方法都失败了", } } private async captureWithHtmlToImage(element: HTMLElement, options: ScreenshotOptions): Promise { try { // 等待字体和图片加载 await this.waitForAssets(element) const config = { quality: options.quality || 0.95, width: options.width, height: options.height, backgroundColor: options.backgroundColor || "#ffffff", skipFonts: options.skipFonts || false, preferredFontFormat: options.preferredFontFormat || "woff2", pixelRatio: options.scale || window.devicePixelRatio || 1, cacheBust: true, useCORS: true, allowTaint: true, style: { transform: "scale(1)", transformOrigin: "top left", }, } let dataUrl: string let blob: Blob | undefined switch (options.format) { case "jpeg": dataUrl = await toJpeg(element, config) blob = await toBlob(element, config) break case "svg": dataUrl = await toSvg(element, config) break default: dataUrl = await toPng(element, config) blob = await toBlob(element, config) } return { success: true, dataUrl, blob, method: "html-to-image", } } catch (error) { throw new Error(`html-to-image 截图失败: ${error}`) } } private async captureWithHtml2Canvas(element: HTMLElement, options: ScreenshotOptions): Promise { try { await this.waitForAssets(element) const canvas = await html2canvas(element, { allowTaint: true, useCORS: true, scale: options.scale || window.devicePixelRatio || 1, width: options.width, height: options.height, backgroundColor: options.backgroundColor || "#ffffff", logging: false, removeContainer: true, imageTimeout: options.timeout || 15000, onclone: (clonedDoc) => { // 确保克隆的文档样式正确 const clonedElement = clonedDoc.querySelector(`[data-screenshot-target]`) if (clonedElement) { clonedElement.style.transform = "none" clonedElement.style.position = "static" } }, }) const dataUrl = canvas.toDataURL(`image/${options.format || "png"}`, options.quality || 0.95) return new Promise((resolve) => { canvas.toBlob( (blob) => { resolve({ success: true, dataUrl, blob: blob || undefined, method: "html2canvas", }) }, `image/${options.format || "png"}`, options.quality || 0.95, ) }) } catch (error) { throw new Error(`html2canvas 截图失败: ${error}`) } } private async captureWithCanvas(element: HTMLElement, options: ScreenshotOptions): Promise { try { const rect = element.getBoundingClientRect() const canvas = document.createElement("canvas") const ctx = canvas.getContext("2d") if (!ctx) { throw new Error("无法获取Canvas上下文") } const scale = options.scale || window.devicePixelRatio || 1 canvas.width = (options.width || rect.width) * scale canvas.height = (options.height || rect.height) * scale ctx.scale(scale, scale) ctx.fillStyle = options.backgroundColor || "#ffffff" ctx.fillRect(0, 0, canvas.width / scale, canvas.height / scale) // 使用SVG foreignObject来渲染HTML const svgData = await this.elementToSVG(element) const img = new Image() return new Promise((resolve, reject) => { img.onload = () => { ctx.drawImage(img, 0, 0) const dataUrl = canvas.toDataURL(`image/${options.format || "png"}`, options.quality || 0.95) canvas.toBlob( (blob) => { resolve({ success: true, dataUrl, blob: blob || undefined, method: "canvas", }) }, `image/${options.format || "png"}`, options.quality || 0.95, ) } img.onerror = () => reject(new Error("图片加载失败")) img.src = `data:image/svg+xml;base64,${btoa(svgData)}` }) } catch (error) { throw new Error(`Canvas 截图失败: ${error}`) } } private async captureWithSVG(element: HTMLElement, options: ScreenshotOptions): Promise { try { const svgData = await this.elementToSVG(element) const dataUrl = `data:image/svg+xml;base64,${btoa(svgData)}` if (options.format === "svg") { return { success: true, dataUrl, method: "svg", } } // 转换SVG为其他格式 const canvas = document.createElement("canvas") const ctx = canvas.getContext("2d") if (!ctx) { throw new Error("无法获取Canvas上下文") } const img = new Image() return new Promise((resolve, reject) => { img.onload = () => { const scale = options.scale || 1 canvas.width = (options.width || img.width) * scale canvas.height = (options.height || img.height) * scale ctx.scale(scale, scale) ctx.fillStyle = options.backgroundColor || "#ffffff" ctx.fillRect(0, 0, canvas.width / scale, canvas.height / scale) ctx.drawImage(img, 0, 0) const finalDataUrl = canvas.toDataURL(`image/${options.format || "png"}`, options.quality || 0.95) canvas.toBlob( (blob) => { resolve({ success: true, dataUrl: finalDataUrl, blob: blob || undefined, method: "svg-to-canvas", }) }, `image/${options.format || "png"}`, options.quality || 0.95, ) } img.onerror = () => reject(new Error("SVG转换失败")) img.src = dataUrl }) } catch (error) { throw new Error(`SVG 截图失败: ${error}`) } } private async elementToSVG(element: HTMLElement): Promise { const rect = element.getBoundingClientRect() const computedStyle = window.getComputedStyle(element) // 获取所有样式 const styles = this.getAllStyles() const svg = `
${element.outerHTML}
` return svg } private getAllStyles(): string { let styles = "" // 获取所有样式表 for (let i = 0; i < document.styleSheets.length; i++) { try { const styleSheet = document.styleSheets[i] if (styleSheet.cssRules) { for (let j = 0; j < styleSheet.cssRules.length; j++) { styles += styleSheet.cssRules[j].cssText + "\n" } } } catch (e) { // 跨域样式表可能无法访问 console.warn("无法访问样式表:", e) } } return styles } private async waitForAssets(element: HTMLElement): Promise { const images = element.querySelectorAll("img") const promises: Promise[] = [] images.forEach((img) => { if (!img.complete) { promises.push( new Promise((resolve) => { img.onload = () => resolve() img.onerror = () => resolve() // 即使加载失败也继续 setTimeout(() => resolve(), 5000) // 5秒超时 }), ) } }) // 等待字体加载 if (document.fonts) { promises.push(document.fonts.ready.catch(() => {})) } await Promise.all(promises) // 额外等待确保渲染完成 await this.delay(500) } private delay(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)) } async captureFullPage(options: ScreenshotOptions = {}): Promise { try { // 标记body元素用于截图 document.body.setAttribute("data-screenshot-target", "true") const result = await this.captureElement(document.body, { ...options, width: window.innerWidth, height: document.body.scrollHeight, }) document.body.removeAttribute("data-screenshot-target") return result } catch (error) { document.body.removeAttribute("data-screenshot-target") throw error } } async captureViewport(options: ScreenshotOptions = {}): Promise { try { // 创建一个包含当前视口的容器 const viewport = document.createElement("div") viewport.style.position = "fixed" viewport.style.top = "0" viewport.style.left = "0" viewport.style.width = "100vw" viewport.style.height = "100vh" viewport.style.pointerEvents = "none" viewport.style.zIndex = "9999" // 克隆当前视口内容 const bodyClone = document.body.cloneNode(true) as HTMLElement viewport.appendChild(bodyClone) document.body.appendChild(viewport) const result = await this.captureElement(viewport, { ...options, width: window.innerWidth, height: window.innerHeight, }) document.body.removeChild(viewport) return result } catch (error) { throw new Error(`视口截图失败: ${error}`) } } } export const enhancedScreenshotService = new EnhancedScreenshotService()