refactor: overhaul UI for streamlined user experience
Redesign navigation, home overview, user portrait, and valuation pages with improved functionality and responsive design. Co-authored-by: null <4804959+fnvtk@users.noreply.github.com>
This commit is contained in:
531
lib/documentation/screenshot-service.ts
Normal file
531
lib/documentation/screenshot-service.ts
Normal file
@@ -0,0 +1,531 @@
|
||||
/**
|
||||
* 增强版截图服务
|
||||
* 结合多种技术方案确保能够成功捕获页面截图
|
||||
*/
|
||||
|
||||
// 动态导入所需库
|
||||
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: 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<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 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<string | null> {
|
||||
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<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: 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<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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user