Files
users/lib/documentation/screenshot-service.ts
v0 4ea8963ddc fix: resolve build errors and complete missing files
Fix CSS issues, add missing files, and optimize documentation page.

Co-authored-by: null <4804959+fnvtk@users.noreply.github.com>
2025-07-19 02:39:56 +00:00

687 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 增强版截图服务
* 结合多种技术方案确保能够成功捕获页面截图
*/
export interface ScreenshotOptions {
format?: "png" | "jpeg" | "webp"
quality?: number
scale?: number
backgroundColor?: string
timeout?: number
width?: number
height?: number
}
export interface ScreenshotResult {
success: boolean
dataUrl?: string
error?: string
method?: string
}
class ScreenshotService {
async captureViewport(options: ScreenshotOptions = {}): Promise<ScreenshotResult> {
try {
// 方法1: 使用 html2canvas
if (typeof window !== "undefined" && window.html2canvas) {
const canvas = await window.html2canvas(document.body, {
backgroundColor: options.backgroundColor || "#ffffff",
scale: options.scale || 1,
width: options.width,
height: options.height,
useCORS: true,
allowTaint: true,
})
const dataUrl = canvas.toDataURL(`image/${options.format || "png"}`, options.quality || 0.9)
return {
success: true,
dataUrl,
method: "html2canvas",
}
}
// 方法2: 使用 dom-to-image
if (typeof window !== "undefined" && window.domtoimage) {
const dataUrl = await window.domtoimage.toPng(document.body, {
bgcolor: options.backgroundColor || "#ffffff",
width: options.width,
height: options.height,
style: {
transform: `scale(${options.scale || 1})`,
transformOrigin: "top left",
},
})
return {
success: true,
dataUrl,
method: "dom-to-image",
}
}
// 方法3: 使用 Canvas API (基础实现)
const canvas = document.createElement("canvas")
const ctx = canvas.getContext("2d")
if (!ctx) {
throw new Error("无法获取Canvas上下文")
}
canvas.width = options.width || window.innerWidth
canvas.height = options.height || window.innerHeight
// 设置背景色
ctx.fillStyle = options.backgroundColor || "#ffffff"
ctx.fillRect(0, 0, canvas.width, canvas.height)
// 简单的文本渲染作为占位符
ctx.fillStyle = "#333333"
ctx.font = "16px Arial"
ctx.fillText("页面截图占位符", 50, 50)
ctx.fillText(`URL: ${window.location.href}`, 50, 80)
ctx.fillText(`时间: ${new Date().toLocaleString()}`, 50, 110)
const dataUrl = canvas.toDataURL(`image/${options.format || "png"}`, options.quality || 0.9)
return {
success: true,
dataUrl,
method: "canvas-fallback",
}
} catch (error) {
console.error("截图失败:", error)
return {
success: false,
error: error instanceof Error ? error.message : "未知错误",
}
}
}
async captureElement(element: HTMLElement, options: ScreenshotOptions = {}): Promise<ScreenshotResult> {
try {
// 使用 html2canvas 截取特定元素
if (typeof window !== "undefined" && window.html2canvas) {
const canvas = await window.html2canvas(element, {
backgroundColor: options.backgroundColor || "#ffffff",
scale: options.scale || 1,
width: options.width,
height: options.height,
useCORS: true,
allowTaint: true,
})
const dataUrl = canvas.toDataURL(`image/${options.format || "png"}`, options.quality || 0.9)
return {
success: true,
dataUrl,
method: "html2canvas-element",
}
}
// 使用 dom-to-image 截取特定元素
if (typeof window !== "undefined" && window.domtoimage) {
const dataUrl = await window.domtoimage.toPng(element, {
bgcolor: options.backgroundColor || "#ffffff",
width: options.width,
height: options.height,
})
return {
success: true,
dataUrl,
method: "dom-to-image-element",
}
}
throw new Error("没有可用的截图库")
} catch (error) {
console.error("元素截图失败:", error)
return {
success: false,
error: error instanceof Error ? error.message : "未知错误",
}
}
}
}
export const screenshotService = new ScreenshotService()
// 扩展 Window 接口以包含截图库
declare global {
interface Window {
html2canvas?: any
domtoimage?: any
}
}
// 动态导入所需库
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<string> {
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: options.quality || 0.95,
cacheBust: true,
pixelRatio: options.scale || 2,
skipAutoScale: true,
style: {
"background-color": options.backgroundColor || "#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: options.quality || 0.95,
bgcolor: options.backgroundColor || "#ffffff",
scale: options.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<string | null> {
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 svgUrl = URL.createObjectURL(svg)
// 等待图像加载
await new Promise((resolve, reject) => {
img.onload = resolve
img.onerror = reject
img.src = svgUrl
})
// 绘制图像
ctx.drawImage(img, 0, 0, width, height)
URL.revokeObjectURL(svgUrl)
// 返回数据URL
return canvas.toDataURL("image/png")
} catch (error) {
console.error("Canvas捕获失败:", error)
return null
}
}
/**
* 使用postMessage与iframe通信捕获截图
*/
async function captureWithPostMessage(iframe: HTMLIFrameElement): Promise<string | null> {
return new Promise((resolve, reject) => {
// 设置超时
const timeout = setTimeout(() => {
window.removeEventListener("message", messageHandler)
reject(new Error("postMessage截图超时"))
}, options.timeout || 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<string | null> {
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: ${options.scale || 2},
backgroundColor: '${options.backgroundColor || "#ffffff"}'
});
// 发送截图数据
const dataUrl = canvas.toDataURL('image/${options.format || "png"}', ${options.quality || 0.9});
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<string | null> {
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
}
}