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>
This commit is contained in:
v0
2025-07-19 02:39:56 +00:00
parent 901763fae0
commit 4ea8963ddc
8 changed files with 408 additions and 1064 deletions

View File

@@ -5,7 +5,6 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea" import { Textarea } from "@/components/ui/textarea"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Checkbox } from "@/components/ui/checkbox" import { Checkbox } from "@/components/ui/checkbox"
import { Progress } from "@/components/ui/progress" import { Progress } from "@/components/ui/progress"
import { Badge } from "@/components/ui/badge" import { Badge } from "@/components/ui/badge"
@@ -30,7 +29,10 @@ import {
Clock, Clock,
Zap, Zap,
} from "lucide-react" } from "lucide-react"
import { enhancedScreenshotService, type ScreenshotOptions } from "@/lib/documentation/enhanced-screenshot-service" import {
enhancedScreenshotService,
type EnhancedScreenshotOptions,
} from "@/lib/documentation/enhanced-screenshot-service"
import { generateDocx } from "@/lib/documentation/docx-generator" import { generateDocx } from "@/lib/documentation/docx-generator"
import { pageRegistry } from "@/lib/documentation/page-registry" import { pageRegistry } from "@/lib/documentation/page-registry"
@@ -79,18 +81,20 @@ export default function DocumentationPage() {
}) })
const previewRef = useRef<HTMLIFrameElement>(null) const previewRef = useRef<HTMLIFrameElement>(null)
const [screenshotOptions, setScreenshotOptions] = useState<ScreenshotOptions>({ const [screenshotOptions, setScreenshotOptions] = useState<EnhancedScreenshotOptions>({
format: "png", format: "png",
quality: 0.9, quality: 0.9,
scale: 1, scale: 1,
backgroundColor: "#ffffff", backgroundColor: "#ffffff",
timeout: 30000, timeout: 30000,
waitForImages: true,
waitForFonts: true,
addWatermark: true,
}) })
const pages = pageRegistry.getAllPages() const pages = pageRegistry.getAllPages()
useEffect(() => { useEffect(() => {
// 初始化选中所有页面
setSelectedPages(pages.map((p) => p.path)) setSelectedPages(pages.map((p) => p.path))
}, []) }, [])
@@ -131,80 +135,13 @@ export default function DocumentationPage() {
} }
try { try {
// 方法1: 尝试直接截图当前页面(如果是当前页面) const result = await enhancedScreenshotService.captureViewport({
if (window.location.pathname === page.path) { ...screenshotOptions,
const result = await enhancedScreenshotService.captureViewport({ ...getViewportSize(),
...screenshotOptions, watermarkText: `${page.name} - ${new Date().toLocaleString()}`,
...getViewportSize(),
})
if (result.success && result.dataUrl) {
return {
...screenshot,
status: "success",
dataUrl: result.dataUrl,
method: result.method,
size: getViewportSize(),
}
}
}
// 方法2: 使用新窗口截图
const newWindow = window.open(
page.path,
"_blank",
`width=${getViewportSize().width},height=${getViewportSize().height}`,
)
if (!newWindow) {
throw new Error("无法打开新窗口,请检查浏览器弹窗设置")
}
// 等待页面加载
await new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
newWindow.close()
reject(new Error("页面加载超时"))
}, 15000)
const checkLoaded = () => {
try {
if (newWindow.document && newWindow.document.readyState === "complete") {
clearTimeout(timeout)
resolve(void 0)
} else {
setTimeout(checkLoaded, 100)
}
} catch (e) {
// 跨域问题,等待一段时间后继续
setTimeout(checkLoaded, 100)
}
}
newWindow.addEventListener("load", () => {
clearTimeout(timeout)
setTimeout(resolve, 2000) // 额外等待2秒确保渲染完成
})
checkLoaded()
}) })
// 尝试截图新窗口 if (result.success && result.dataUrl) {
let result
try {
if (newWindow.document && newWindow.document.body) {
result = await enhancedScreenshotService.captureElement(newWindow.document.body, {
...screenshotOptions,
...getViewportSize(),
})
}
} catch (e) {
console.warn("新窗口截图失败,尝试其他方法:", e)
}
newWindow.close()
if (result?.success && result.dataUrl) {
return { return {
...screenshot, ...screenshot,
status: "success", status: "success",
@@ -212,52 +149,9 @@ export default function DocumentationPage() {
method: result.method, method: result.method,
size: getViewportSize(), size: getViewportSize(),
} }
} else {
throw new Error(result.error || "截图失败")
} }
// 方法3: 使用iframe截图最后的备选方案
const iframe = document.createElement("iframe")
iframe.style.position = "absolute"
iframe.style.left = "-9999px"
iframe.style.width = `${getViewportSize().width}px`
iframe.style.height = `${getViewportSize().height}px`
iframe.src = page.path
document.body.appendChild(iframe)
try {
await new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error("iframe加载超时"))
}, 15000)
iframe.onload = () => {
clearTimeout(timeout)
setTimeout(resolve, 2000) // 等待渲染完成
}
})
// 尝试截图iframe内容
if (iframe.contentDocument && iframe.contentDocument.body) {
result = await enhancedScreenshotService.captureElement(iframe.contentDocument.body, {
...screenshotOptions,
...getViewportSize(),
})
}
} finally {
document.body.removeChild(iframe)
}
if (result?.success && result.dataUrl) {
return {
...screenshot,
status: "success",
dataUrl: result.dataUrl,
method: result.method,
size: getViewportSize(),
}
}
throw new Error("所有截图方法都失败了")
} catch (error) { } catch (error) {
console.error(`截图失败 ${page.name}:`, error) console.error(`截图失败 ${page.name}:`, error)
return { return {
@@ -288,10 +182,8 @@ export default function DocumentationPage() {
for (let i = 0; i < selectedPageObjects.length; i++) { for (let i = 0; i < selectedPageObjects.length; i++) {
const page = selectedPageObjects[i] const page = selectedPageObjects[i]
// 更新进度
setCaptureProgress((i / selectedPageObjects.length) * 100) setCaptureProgress((i / selectedPageObjects.length) * 100)
// 添加待处理的截图
const pendingScreenshot: Screenshot = { const pendingScreenshot: Screenshot = {
id: `${page.path}-${Date.now()}`, id: `${page.path}-${Date.now()}`,
name: page.name, name: page.name,
@@ -306,7 +198,6 @@ export default function DocumentationPage() {
try { try {
const screenshot = await captureScreenshot(page) const screenshot = await captureScreenshot(page)
// 更新截图状态
const updatedScreenshots = [...newScreenshots] const updatedScreenshots = [...newScreenshots]
updatedScreenshots[i] = screenshot updatedScreenshots[i] = screenshot
setScreenshots(updatedScreenshots) setScreenshots(updatedScreenshots)
@@ -338,7 +229,6 @@ export default function DocumentationPage() {
newScreenshots[i] = failedScreenshot newScreenshots[i] = failedScreenshot
} }
// 添加延迟避免过快请求
if (i < selectedPageObjects.length - 1) { if (i < selectedPageObjects.length - 1) {
await new Promise((resolve) => setTimeout(resolve, 1000)) await new Promise((resolve) => setTimeout(resolve, 1000))
} }
@@ -357,62 +247,6 @@ export default function DocumentationPage() {
}) })
} }
const retryFailedScreenshots = async () => {
const failedScreenshots = screenshots.filter((s) => s.status === "failed")
if (failedScreenshots.length === 0) {
toast({
title: "没有失败的截图",
description: "所有截图都已成功",
})
return
}
setIsCapturing(true)
setCaptureProgress(0)
for (let i = 0; i < failedScreenshots.length; i++) {
const failedScreenshot = failedScreenshots[i]
const page = pages.find((p) => p.path === failedScreenshot.url)
if (!page) continue
setCaptureProgress((i / failedScreenshots.length) * 100)
// 更新状态为重新截图中
setScreenshots((prev) =>
prev.map((s) => (s.id === failedScreenshot.id ? { ...s, status: "capturing" as const } : s)),
)
try {
const newScreenshot = await captureScreenshot(page)
setScreenshots((prev) => prev.map((s) => (s.id === failedScreenshot.id ? newScreenshot : s)))
if (newScreenshot.status === "success") {
toast({
title: "重试成功",
description: `${page.name} 截图完成`,
})
}
} catch (error) {
console.error(`重试截图失败:`, error)
setScreenshots((prev) =>
prev.map((s) =>
s.id === failedScreenshot.id
? { ...s, status: "failed" as const, error: error instanceof Error ? error.message : "未知错误" }
: s,
),
)
}
await new Promise((resolve) => setTimeout(resolve, 1000))
}
setCaptureProgress(100)
setIsCapturing(false)
}
const generateDocument = async () => { const generateDocument = async () => {
const successfulScreenshots = screenshots.filter((s) => s.status === "success" && s.dataUrl) const successfulScreenshots = screenshots.filter((s) => s.status === "success" && s.dataUrl)
@@ -430,7 +264,6 @@ export default function DocumentationPage() {
try { try {
const docBuffer = await generateDocx(successfulScreenshots, documentSettings) const docBuffer = await generateDocx(successfulScreenshots, documentSettings)
// 下载文档
const blob = new Blob([docBuffer], { const blob = new Blob([docBuffer], {
type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
}) })
@@ -520,9 +353,8 @@ export default function DocumentationPage() {
</div> </div>
<Tabs defaultValue="capture" className="space-y-6"> <Tabs defaultValue="capture" className="space-y-6">
<TabsList className="grid w-full grid-cols-4"> <TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="capture"></TabsTrigger> <TabsTrigger value="capture"></TabsTrigger>
<TabsTrigger value="preview"></TabsTrigger>
<TabsTrigger value="screenshots"></TabsTrigger> <TabsTrigger value="screenshots"></TabsTrigger>
<TabsTrigger value="settings"></TabsTrigger> <TabsTrigger value="settings"></TabsTrigger>
</TabsList> </TabsList>
@@ -583,105 +415,6 @@ export default function DocumentationPage() {
</div> </div>
<div className="space-y-6"> <div className="space-y-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center space-x-2">
<Settings className="h-5 w-5" />
<span></span>
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<label className="text-sm font-medium"></label>
<Select
value={previewMode}
onValueChange={(value: "desktop" | "tablet" | "mobile") => setPreviewMode(value)}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="desktop">
<div className="flex items-center space-x-2">
<Monitor className="h-4 w-4" />
<span> (1920x1080)</span>
</div>
</SelectItem>
<SelectItem value="tablet">
<div className="flex items-center space-x-2">
<Tablet className="h-4 w-4" />
<span> (768x1024)</span>
</div>
</SelectItem>
<SelectItem value="mobile">
<div className="flex items-center space-x-2">
<Smartphone className="h-4 w-4" />
<span> (375x667)</span>
</div>
</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<label className="text-sm font-medium"></label>
<Select
value={screenshotOptions.format}
onValueChange={(value: "png" | "jpeg" | "webp") =>
setScreenshotOptions((prev) => ({ ...prev, format: value }))
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="png">PNG ()</SelectItem>
<SelectItem value="jpeg">JPEG ()</SelectItem>
<SelectItem value="webp">WebP ()</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">
: {Math.round((screenshotOptions.quality || 0.9) * 100)}%
</label>
<input
type="range"
min="0.1"
max="1"
step="0.1"
value={screenshotOptions.quality || 0.9}
onChange={(e) =>
setScreenshotOptions((prev) => ({
...prev,
quality: Number.parseFloat(e.target.value),
}))
}
className="w-full"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">: {screenshotOptions.scale || 1}x</label>
<input
type="range"
min="0.5"
max="3"
step="0.1"
value={screenshotOptions.scale || 1}
onChange={(e) =>
setScreenshotOptions((prev) => ({
...prev,
scale: Number.parseFloat(e.target.value),
}))
}
className="w-full"
/>
</div>
</CardContent>
</Card>
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="flex items-center space-x-2"> <CardTitle className="flex items-center space-x-2">
@@ -700,37 +433,23 @@ export default function DocumentationPage() {
</div> </div>
)} )}
<div className="space-y-2"> <Button
<Button onClick={startCapture}
onClick={startCapture} disabled={isCapturing || selectedPages.length === 0}
disabled={isCapturing || selectedPages.length === 0} className="w-full"
className="w-full" >
> {isCapturing ? (
{isCapturing ? ( <>
<> <RefreshCw className="mr-2 h-4 w-4 animate-spin" />
<RefreshCw className="mr-2 h-4 w-4 animate-spin" /> ...
... </>
</> ) : (
) : ( <>
<> <Camera className="mr-2 h-4 w-4" />
<Camera className="mr-2 h-4 w-4" />
</>
</>
)}
</Button>
{screenshots.some((s) => s.status === "failed") && (
<Button
variant="outline"
onClick={retryFailedScreenshots}
disabled={isCapturing}
className="w-full bg-transparent"
>
<RefreshCw className="mr-2 h-4 w-4" />
</Button>
)} )}
</div> </Button>
<div className="text-sm text-muted-foreground"> <div className="text-sm text-muted-foreground">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@@ -752,65 +471,6 @@ export default function DocumentationPage() {
</div> </div>
</TabsContent> </TabsContent>
<TabsContent value="preview" className="space-y-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center justify-between">
<div className="flex items-center space-x-2">
{getPreviewIcon()}
<span></span>
</div>
<div className="flex items-center space-x-2">
<Select
value={previewMode}
onValueChange={(value: "desktop" | "tablet" | "mobile") => setPreviewMode(value)}
>
<SelectTrigger className="w-[150px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="desktop"></SelectItem>
<SelectItem value="tablet"></SelectItem>
<SelectItem value="mobile"></SelectItem>
</SelectContent>
</Select>
</div>
</CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<div className="border rounded-lg overflow-hidden bg-gray-100 p-4">
<div
className="mx-auto bg-white shadow-lg rounded-lg overflow-hidden"
style={{
width: `${getViewportSize().width}px`,
height: `${getViewportSize().height}px`,
maxWidth: "100%",
transform: "scale(0.5)",
transformOrigin: "top center",
}}
>
{currentPreviewUrl ? (
<iframe
ref={previewRef}
src={currentPreviewUrl}
className="w-full h-full border-0"
title="页面预览"
/>
) : (
<div className="flex items-center justify-center h-full text-muted-foreground">
<div className="text-center">
<Globe className="h-12 w-12 mx-auto mb-4 opacity-50" />
<p></p>
</div>
</div>
)}
</div>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="screenshots" className="space-y-6"> <TabsContent value="screenshots" className="space-y-6">
<Card> <Card>
<CardHeader> <CardHeader>
@@ -974,43 +634,6 @@ export default function DocumentationPage() {
</div> </div>
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-2">
<label className="text-sm font-medium"></label>
<Select
value={documentSettings.pageSize}
onValueChange={(value: "A4" | "A3" | "Letter") =>
setDocumentSettings((prev) => ({ ...prev, pageSize: value }))
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="A4">A4</SelectItem>
<SelectItem value="A3">A3</SelectItem>
<SelectItem value="Letter">Letter</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<label className="text-sm font-medium"></label>
<Select
value={documentSettings.orientation}
onValueChange={(value: "portrait" | "landscape") =>
setDocumentSettings((prev) => ({ ...prev, orientation: value }))
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="portrait"></SelectItem>
<SelectItem value="landscape"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-3"> <div className="space-y-3">
<label className="text-sm font-medium"></label> <label className="text-sm font-medium"></label>
<div className="space-y-2"> <div className="space-y-2">

View File

@@ -4,7 +4,6 @@
@layer base { @layer base {
:root { :root {
/* 更新为毛玻璃风格的颜色变量 */
--background: 240 10% 98%; --background: 240 10% 98%;
--foreground: 240 10% 3.9%; --foreground: 240 10% 3.9%;
--card: 240 10% 100%; --card: 240 10% 100%;
@@ -70,7 +69,6 @@
@apply bg-gradient-to-br from-gray-900 via-gray-800 to-gray-900; @apply bg-gradient-to-br from-gray-900 via-gray-800 to-gray-900;
} }
/* 移动端优化 */
@media (max-width: 768px) { @media (max-width: 768px) {
body { body {
background-attachment: scroll; background-attachment: scroll;
@@ -79,28 +77,30 @@
} }
@layer components { @layer components {
/* 毛玻璃效果基础类 */
.glass { .glass {
@apply backdrop-blur-md bg-white/10 border border-white/20; @apply backdrop-blur-md border border-white/20;
background-color: rgba(255, 255, 255, 0.1);
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37); box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
} }
.glass-light { .glass-light {
@apply backdrop-blur-sm bg-white/20 border border-white/30; @apply backdrop-blur-sm border border-white/30;
background-color: rgba(255, 255, 255, 0.2);
box-shadow: 0 4px 16px 0 rgba(31, 38, 135, 0.2); box-shadow: 0 4px 16px 0 rgba(31, 38, 135, 0.2);
} }
.glass-heavy { .glass-heavy {
@apply backdrop-blur-xl bg-white/30 border border-white/40; @apply backdrop-blur-xl border border-white/40;
background-color: rgba(255, 255, 255, 0.3);
box-shadow: 0 16px 64px 0 rgba(31, 38, 135, 0.5); box-shadow: 0 16px 64px 0 rgba(31, 38, 135, 0.5);
} }
.glass-dark { .glass-dark {
@apply backdrop-blur-md bg-black/10 border border-white/10; @apply backdrop-blur-md border border-white/10;
background-color: rgba(0, 0, 0, 0.1);
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.3); box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.3);
} }
/* 移动端毛玻璃效果优化 */
@media (max-width: 768px) { @media (max-width: 768px) {
.glass { .glass {
@apply backdrop-blur-sm; @apply backdrop-blur-sm;
@@ -118,42 +118,49 @@
} }
} }
/* 卡片毛玻璃效果 */
.glass-card { .glass-card {
@apply glass rounded-2xl p-6 transition-all duration-300 hover:bg-white/20 hover:shadow-glass-lg; @apply glass rounded-2xl p-6 transition-all duration-300;
}
.glass-card:hover {
background-color: rgba(255, 255, 255, 0.2);
} }
/* 移动端卡片优化 */
@media (max-width: 768px) { @media (max-width: 768px) {
.glass-card { .glass-card {
@apply p-4 rounded-xl; @apply p-4 rounded-xl;
} }
} }
/* 导航栏毛玻璃效果 */
.glass-nav { .glass-nav {
@apply glass-light rounded-2xl transition-all duration-300; @apply glass-light rounded-2xl transition-all duration-300;
} }
/* 按钮毛玻璃效果 */
.glass-button { .glass-button {
@apply glass-light rounded-xl px-4 py-2 transition-all duration-300 hover:bg-white/30 hover:scale-105; @apply glass-light rounded-xl px-4 py-2 transition-all duration-300;
}
.glass-button:hover {
background-color: rgba(255, 255, 255, 0.3);
transform: scale(1.05);
} }
/* 移动端按钮优化 */
@media (max-width: 768px) { @media (max-width: 768px) {
.glass-button { .glass-button {
@apply px-6 py-3 text-base; @apply px-6 py-3 text-base;
min-height: 44px; /* iOS推荐的最小触摸目标 */ min-height: 44px;
} }
} }
/* 输入框毛玻璃效果 */
.glass-input { .glass-input {
@apply glass-light rounded-xl px-4 py-2 transition-all duration-300 focus:bg-white/30 focus:ring-2 focus:ring-white/50; @apply glass-light rounded-xl px-4 py-2 transition-all duration-300;
}
.glass-input:focus {
background-color: rgba(255, 255, 255, 0.3);
@apply ring-2 ring-white/50;
} }
/* 移动端输入框优化 */
@media (max-width: 768px) { @media (max-width: 768px) {
.glass-input { .glass-input {
@apply px-4 py-3 text-base; @apply px-4 py-3 text-base;
@@ -161,7 +168,6 @@
} }
} }
/* 安全区域适配 */
.safe-area-top { .safe-area-top {
padding-top: env(safe-area-inset-top); padding-top: env(safe-area-inset-top);
} }
@@ -179,7 +185,6 @@
} }
} }
/* 移动端滚动优化 */
@media (max-width: 768px) { @media (max-width: 768px) {
.overflow-scroll { .overflow-scroll {
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;

View File

@@ -1,24 +1,4 @@
/** import { Document, Packer, Paragraph, ImageRun, TextRun, HeadingLevel } from "docx"
* Word文档生成器
* 使用docx库生成专业的Word文档
*/
import {
Document,
Paragraph,
TextRun,
HeadingLevel,
ImageRun,
TableOfContents,
PageBreak,
AlignmentType,
Table,
TableRow,
TableCell,
WidthType,
BorderStyle,
} from "docx"
import { Buffer } from "buffer"
interface Screenshot { interface Screenshot {
id: string id: string
@@ -41,445 +21,190 @@ interface DocumentSettings {
orientation: string orientation: string
} }
interface DocumentSection {
title: string
path: string
description: string
screenshot: string
}
interface DocumentData {
title: string
author: string
date: string
sections: DocumentSection[]
}
/**
* 将base64字符串转换为Buffer
* @param base64 base64字符串
* @returns Buffer
*/
function base64ToBuffer(base64: string): Buffer {
try {
// 移除data URL前缀
const base64Data = base64.includes("base64,") ? base64.split("base64,")[1] : base64
// 转换为Buffer
return Buffer.from(base64Data, "base64")
} catch (error) {
console.error("base64转换为Buffer时出错:", error)
throw error
}
}
/**
* 生成Word文档
* @param screenshots 截图数据
* @param settings 文档设置
* @returns 文档的ArrayBuffer
*/
export async function generateDocx(screenshots: Screenshot[], settings: DocumentSettings): Promise<ArrayBuffer> { export async function generateDocx(screenshots: Screenshot[], settings: DocumentSettings): Promise<ArrayBuffer> {
console.log("开始生成Word文档...")
try { try {
// 验证输入数据 const children: Paragraph[] = []
if (!screenshots || !Array.isArray(screenshots)) {
throw new Error("截图数据不能为空或不是数组")
}
if (!settings) { // 添加标题
throw new Error("文档设置不能为空") children.push(
} new Paragraph({
children: [
console.log(`文档标题: ${settings.title}`) new TextRun({
console.log(`作者: ${settings.author}`) text: settings.title,
console.log(`描述: ${settings.description}`) bold: true,
console.log(`部分数量: ${screenshots.length}`) size: 32,
}),
// 创建文档
const doc = new Document({
title: settings.title,
description: settings.description,
creator: settings.author,
styles: {
paragraphStyles: [
{
id: "Normal",
name: "Normal",
run: {
size: 24, // 12pt
font: "Microsoft YaHei",
},
paragraph: {
spacing: {
line: 360, // 1.5倍行距
before: 240, // 12pt
after: 240, // 12pt
},
},
},
{
id: "Heading1",
name: "Heading 1",
run: {
size: 36, // 18pt
bold: true,
font: "Microsoft YaHei",
},
paragraph: {
spacing: {
before: 480, // 24pt
after: 240, // 12pt
},
},
},
{
id: "Heading2",
name: "Heading 2",
run: {
size: 32, // 16pt
bold: true,
font: "Microsoft YaHei",
},
paragraph: {
spacing: {
before: 360, // 18pt
after: 240, // 12pt
},
},
},
{
id: "Caption",
name: "Caption",
run: {
size: 20, // 10pt
italic: true,
font: "Microsoft YaHei",
},
paragraph: {
alignment: AlignmentType.CENTER,
spacing: {
before: 120, // 6pt
after: 240, // 12pt
},
},
},
], ],
}) heading: HeadingLevel.TITLE,
}),
)
// 文档部分 // 添加描述
const sections = [] if (settings.description) {
children.push(
// 封面
sections.push({
properties: {},
children: [
new Paragraph({ new Paragraph({
text: "",
spacing: {\
before: 3000, // 大约页面1/3处
},\
}),\
new Paragraph({\
text: settings.title,\
heading: HeadingLevel.TITLE,
alignment: AlignmentType.CENTER,
spacing: {\
after: 400,
},
}),
new Paragraph({\
text: "",\
spacing: {\
before: 800,
},
}),
new Paragraph({
alignment: AlignmentType.CENTER,\
children: [ children: [
new TextRun({ new TextRun({
text: \`作者: ${settings.author}`,\ text: settings.description,
size: 24,\
}),
],
}),
new Paragraph({
alignment: AlignmentType.CENTER,\
children: [
new TextRun({
text: `生成日期: ${new Date().toLocaleString()}`,\
size: 24, size: 24,
}), }),
], ],
}), }),
new Paragraph({ )
text: "",\
break: PageBreak.AFTER,
}),
],
})
// 目录
sections.push({
properties: {},
children: [\
new Paragraph({
text: "目录",
heading: HeadingLevel.HEADING_1,\
alignment: AlignmentType.CENTER,
}),
new TableOfContents("目录", {
hyperlink: true,
headingStyleRange: "1-3",\
}),\
new Paragraph({
text: "",
break: PageBreak.AFTER,
}),\
],
})
// 正文
const contentSection = {
properties: {},
children: [] as any[],
} }
// 添加简介 // 添加生成信息
contentSection.children.push(\ if (settings.includeTimestamp) {
children.push(
new Paragraph({
children: [
new TextRun({
text: `生成时间: ${new Date().toLocaleString()}`,
size: 20,
italics: true,
}),
],
}),
)
}
children.push(
new Paragraph({ new Paragraph({
text: "1. 简介", children: [
heading: HeadingLevel.HEADING_1, new TextRun({
}), text: `作者: ${settings.author}`,
new Paragraph({ size: 20,
text: "本文档由文档生成工具自动生成,包含应用程序的所有主要页面截图和功能说明。", italics: true,
}), }),
new Paragraph({ ],
text: "文档目的是帮助用户了解系统功能和使用方法,为系统管理员和最终用户提供参考。",
}),\
new Paragraph({
text: "",
}), }),
) )
// 添加页面内容 // 添加分隔符
contentSection.children.push( children.push(
new Paragraph({ new Paragraph({
text: "2. 系统功能", children: [
heading: HeadingLevel.HEADING_1, new TextRun({
text: "─".repeat(50),
size: 20,
}),
],
}), }),
) )
// 处理每个部分 // 添加每个截图
console.log("开始处理文档部分...") for (const screenshot of screenshots) {
// 页面标题
// 使用for循环而不是forEach以便更好地处理错误 children.push(
for (let i = 0; i < screenshots.length; i++) { new Paragraph({
try { children: [
const screenshot = screenshots[i] new TextRun({
console.log(\`处理部分 ${i + 1}/${screenshots.length}: ${screenshot.name}`) text: screenshot.name,
bold: true,
// 添加标题 size: 28,
contentSection.children.push(
new Paragraph({
text: `2.${i + 1} ${screenshot.name}`,
heading: HeadingLevel.HEADING_2,
}),
)
// 添加页面URL和截图时间
if (settings.includePageUrls) {
contentSection.children.push(
new Paragraph({
text: `页面URL: ${screenshot.url}`,
}), }),
) ],
} heading: HeadingLevel.HEADING_1,
}),
)
if (settings.includeTimestamp) { // 页面URL
contentSection.children.push( if (settings.includePageUrls) {
new Paragraph({ children.push(
text: `截图时间: ${screenshot.timestamp.toLocaleString()}`,
}),
)
}
// 添加状态
contentSection.children.push(
new Paragraph({ new Paragraph({
text: `状态: ${screenshot.status}`, children: [
}), new TextRun({
) text: `页面地址: ${screenshot.url}`,
size: 20,
// 添加截图 color: "666666",
try {
if (screenshot.dataUrl && screenshot.dataUrl.startsWith("data:image/")) {
console.log(`处理截图: ${screenshot.name}`)
// 从base64数据URL中提取图像数据
const base64Data = screenshot.dataUrl.split(",")[1]
if (!base64Data) {
throw new Error("无效的base64数据")
}
// 将base64转换为二进制数据
const imageBuffer = base64ToBuffer(screenshot.dataUrl)
// 添加图像
contentSection.children.push(
new Paragraph({
children: [
new ImageRun({
data: imageBuffer,
transformation: {
width: 600,
height: 400,
},
}),
],
alignment: AlignmentType.CENTER,
}), }),
new Paragraph({ ],
text: `${i + 1}: ${screenshot.name} 页面截图`,
style: "Caption",
}),
new Paragraph({
text: "",
}),
)
} else {
console.warn(`部分 ${i + 1} 缺少有效的截图`)
contentSection.children.push(
new Paragraph({
text: "[截图不可用]",
alignment: AlignmentType.CENTER,
}),
new Paragraph({
text: "",
}),
)
}
} catch (imageError) {
console.error(`处理部分 ${i + 1} 的截图时出错:`, imageError)
contentSection.children.push(
new Paragraph({
text: "[处理截图时出错]",
alignment: AlignmentType.CENTER,
}),
new Paragraph({
text: "",
}),
)
}
} catch (sectionError) {
console.error(`处理部分 ${i + 1} 时出错:`, sectionError)
contentSection.children.push(
new Paragraph({
text: `[处理部分 ${i + 1} 时出错: ${sectionError instanceof Error ? sectionError.message : String(sectionError)}]`,
alignment: AlignmentType.CENTER,
}),
new Paragraph({
text: "",
}), }),
) )
} }
// 截图时间
if (settings.includeTimestamp) {
children.push(
new Paragraph({
children: [
new TextRun({
text: `截图时间: ${screenshot.timestamp.toLocaleString()}`,
size: 20,
color: "666666",
}),
],
}),
)
}
// 添加截图
if (screenshot.dataUrl) {
try {
// 将 dataUrl 转换为 ArrayBuffer
const base64Data = screenshot.dataUrl.split(",")[1]
const binaryString = atob(base64Data)
const bytes = new Uint8Array(binaryString.length)
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i)
}
children.push(
new Paragraph({
children: [
new ImageRun({
data: bytes,
transformation: {
width: 600,
height: 400,
},
}),
],
}),
)
} catch (error) {
console.error("添加图片失败:", error)
children.push(
new Paragraph({
children: [
new TextRun({
text: "[图片加载失败]",
color: "FF0000",
italics: true,
}),
],
}),
)
}
}
// 添加分隔符
children.push(
new Paragraph({
children: [
new TextRun({
text: "",
}),
],
}),
)
} }
// 添加附录 // 创建文档
contentSection.children.push( const doc = new Document({
new Paragraph({ sections: [
text: "3. 附录", {
heading: HeadingLevel.HEADING_1, properties: {},
}), children: children,
new Paragraph({ },
text: "3.1 文档信息",
heading: HeadingLevel.HEADING_2,
}),
)
// 创建文档信息表格
const infoTable = new Table({
width: {
size: 100,
type: WidthType.PERCENTAGE,
},
borders: {
top: { style: BorderStyle.SINGLE, size: 1, color: "auto" },
bottom: { style: BorderStyle.SINGLE, size: 1, color: "auto" },
left: { style: BorderStyle.SINGLE, size: 1, color: "auto" },
right: { style: BorderStyle.SINGLE, size: 1, color: "auto" },
insideHorizontal: { style: BorderStyle.SINGLE, size: 1, color: "auto" },
insideVertical: { style: BorderStyle.SINGLE, size: 1, color: "auto" },
},
rows: [
new TableRow({
children: [
new TableCell({
width: {
size: 30,
type: WidthType.PERCENTAGE,
},
children: [new Paragraph("文档标题")],
}),
new TableCell({
width: {
size: 70,
type: WidthType.PERCENTAGE,
},
children: [new Paragraph(settings.title)],
}),
],
}),
new TableRow({
children: [
new TableCell({
children: [new Paragraph("作者")],
}),
new TableCell({
children: [new Paragraph(settings.author)],
}),
],
}),
new TableRow({
children: [
new TableCell({
children: [new Paragraph("生成日期")],
}),
new TableCell({
children: [new Paragraph(new Date().toLocaleString())],
}),
],
}),
new TableRow({
children: [
new TableCell({
children: [new Paragraph("页面数量")],
}),
new TableCell({
children: [new Paragraph(String(screenshots.length))],
}),
],
}),
], ],
}) })
contentSection.children.push(infoTable) // 生成文档
const buffer = await Packer.toBuffer(doc)
// 添加内容部分到文档
doc.addSection({
children: sections[0].children.concat(sections[1].children, contentSection.children),
})
console.log("文档生成完成,准备导出...")
// 生成blob
const buffer = await doc.save()
console.log("文档生成完成")
return buffer return buffer
} catch (error) { } catch (error) {
console.error("生成Word文档时出错:", error) console.error("生成文档失败:", error)
throw error throw new Error(`文档生成失败: ${error instanceof Error ? error.message : "未知错误"}`)
} }
} }

View File

@@ -1,78 +1,38 @@
interface ScreenshotResult { import { screenshotService, type ScreenshotOptions, type ScreenshotResult } from "./screenshot-service"
success: boolean
dataUrl?: string
error?: string
method?: string
}
interface ScreenshotOptions { export interface EnhancedScreenshotOptions extends ScreenshotOptions {
format?: "png" | "jpeg" | "webp" waitForImages?: boolean
quality?: number waitForFonts?: boolean
scale?: number removeElements?: string[]
width?: number addWatermark?: boolean
height?: number watermarkText?: string
backgroundColor?: string
timeout?: number
} }
class EnhancedScreenshotService { class EnhancedScreenshotService {
async captureViewport(options: ScreenshotOptions = {}): Promise<ScreenshotResult> { async captureViewport(options: EnhancedScreenshotOptions = {}): Promise<ScreenshotResult> {
try { try {
// 创建一个简单的截图占位符 // 等待页面完全加载
const canvas = document.createElement("canvas") await this.waitForPageLoad(options)
canvas.width = options.width || 1200
canvas.height = options.height || 800
const ctx = canvas.getContext("2d")
if (ctx) { // 移除不需要的元素
// 绘制背景 const removedElements = this.removeElements(options.removeElements || [])
ctx.fillStyle = options.backgroundColor || "#f8fafc"
ctx.fillRect(0, 0, canvas.width, canvas.height)
// 绘制标题 try {
ctx.fillStyle = "#1e293b" // 调用基础截图服务
ctx.font = "bold 24px Arial" const result = await screenshotService.captureViewport(options)
ctx.fillText("用户数据资产中台", 50, 60)
// 绘制页面信息 // 添加水印
ctx.fillStyle = "#64748b" if (options.addWatermark && result.success && result.dataUrl) {
ctx.font = "16px Arial" result.dataUrl = await this.addWatermark(result.dataUrl, options.watermarkText)
ctx.fillText(`页面: ${window.location.pathname}`, 50, 100)
ctx.fillText(`截图时间: ${new Date().toLocaleString()}`, 50, 130)
ctx.fillText(`分辨率: ${canvas.width} x ${canvas.height}`, 50, 160)
// 绘制一些装饰性元素
ctx.strokeStyle = "#e2e8f0"
ctx.lineWidth = 2
ctx.strokeRect(30, 30, canvas.width - 60, canvas.height - 60)
// 绘制一些模拟的图表元素
ctx.fillStyle = "#3b82f6"
ctx.fillRect(50, 200, 200, 100)
ctx.fillStyle = "#ffffff"
ctx.font = "14px Arial"
ctx.fillText("数据概览", 60, 230)
ctx.fillText("用户总数: 125,678", 60, 250)
ctx.fillText("活跃用户: 45,678", 60, 270)
ctx.fillStyle = "#10b981"
ctx.fillRect(300, 200, 200, 100)
ctx.fillStyle = "#ffffff"
ctx.fillText("AI分析", 310, 230)
ctx.fillText("预测准确率: 94.2%", 310, 250)
ctx.fillText("异常检测: 3个", 310, 270)
const dataUrl = canvas.toDataURL(`image/${options.format || "png"}`, options.quality || 0.9)
return {
success: true,
dataUrl,
method: "enhanced-canvas",
} }
}
throw new Error("无法创建截图") return result
} finally {
// 恢复移除的元素
this.restoreElements(removedElements)
}
} catch (error) { } catch (error) {
console.error("增强截图失败:", error)
return { return {
success: false, success: false,
error: error instanceof Error ? error.message : "未知错误", error: error instanceof Error ? error.message : "未知错误",
@@ -80,55 +40,131 @@ class EnhancedScreenshotService {
} }
} }
async captureElement(element: HTMLElement, options: ScreenshotOptions = {}): Promise<ScreenshotResult> { async captureElement(element: HTMLElement, options: EnhancedScreenshotOptions = {}): Promise<ScreenshotResult> {
try { try {
// 获取元素的尺寸和位置 // 等待页面完全加载
const rect = element.getBoundingClientRect() await this.waitForPageLoad(options)
const canvas = document.createElement("canvas")
canvas.width = options.width || rect.width
canvas.height = options.height || rect.height
const ctx = canvas.getContext("2d")
if (ctx) { // 调用基础截图服务
// 绘制背景 const result = await screenshotService.captureElement(element, options)
ctx.fillStyle = options.backgroundColor || "#ffffff"
ctx.fillRect(0, 0, canvas.width, canvas.height)
// 绘制元素信息 // 添加水印
ctx.fillStyle = "#1e293b" if (options.addWatermark && result.success && result.dataUrl) {
ctx.font = "16px Arial" result.dataUrl = await this.addWatermark(result.dataUrl, options.watermarkText)
ctx.fillText(`元素截图: ${element.tagName}`, 20, 30)
if (element.textContent) {
ctx.fillStyle = "#64748b"
ctx.font = "14px Arial"
const text = element.textContent.substring(0, 50) + (element.textContent.length > 50 ? "..." : "")
ctx.fillText(`内容: ${text}`, 20, 60)
}
// 绘制边框
ctx.strokeStyle = "#e2e8f0"
ctx.lineWidth = 1
ctx.strokeRect(10, 10, canvas.width - 20, canvas.height - 20)
const dataUrl = canvas.toDataURL(`image/${options.format || "png"}`, options.quality || 0.9)
return {
success: true,
dataUrl,
method: "enhanced-element",
}
} }
throw new Error("无法截取元素") return result
} catch (error) { } catch (error) {
console.error("增强元素截图失败:", error)
return { return {
success: false, success: false,
error: error instanceof Error ? error.message : "未知错误", error: error instanceof Error ? error.message : "未知错误",
} }
} }
} }
private async waitForPageLoad(options: EnhancedScreenshotOptions): Promise<void> {
// 等待图片加载
if (options.waitForImages) {
await this.waitForImages()
}
// 等待字体加载
if (options.waitForFonts) {
await this.waitForFonts()
}
// 额外等待时间确保渲染完成
await new Promise((resolve) => setTimeout(resolve, 1000))
}
private async waitForImages(): Promise<void> {
const images = Array.from(document.images)
const promises = images.map((img) => {
if (img.complete) return Promise.resolve()
return new Promise((resolve, reject) => {
img.onload = resolve
img.onerror = resolve // 即使图片加载失败也继续
setTimeout(resolve, 5000) // 5秒超时
})
})
await Promise.all(promises)
}
private async waitForFonts(): Promise<void> {
if ("fonts" in document) {
try {
await document.fonts.ready
} catch (error) {
console.warn("字体加载等待失败:", error)
}
}
}
private removeElements(selectors: string[]): Array<{ element: Element; parent: Node; nextSibling: Node | null }> {
const removedElements: Array<{ element: Element; parent: Node; nextSibling: Node | null }> = []
selectors.forEach((selector) => {
const elements = document.querySelectorAll(selector)
elements.forEach((element) => {
if (element.parentNode) {
removedElements.push({
element,
parent: element.parentNode,
nextSibling: element.nextSibling,
})
element.parentNode.removeChild(element)
}
})
})
return removedElements
}
private restoreElements(removedElements: Array<{ element: Element; parent: Node; nextSibling: Node | null }>): void {
removedElements.forEach(({ element, parent, nextSibling }) => {
if (nextSibling) {
parent.insertBefore(element, nextSibling)
} else {
parent.appendChild(element)
}
})
}
private async addWatermark(dataUrl: string, watermarkText?: string): Promise<string> {
return new Promise((resolve) => {
const canvas = document.createElement("canvas")
const ctx = canvas.getContext("2d")
const img = new Image()
img.onload = () => {
canvas.width = img.width
canvas.height = img.height
// 绘制原图
ctx?.drawImage(img, 0, 0)
if (ctx && watermarkText) {
// 添加水印
ctx.font = "16px Arial"
ctx.fillStyle = "rgba(0, 0, 0, 0.5)"
ctx.textAlign = "right"
ctx.fillText(
watermarkText || `截图时间: ${new Date().toLocaleString()}`,
canvas.width - 20,
canvas.height - 20,
)
}
resolve(canvas.toDataURL("image/png"))
}
img.src = dataUrl
})
}
} }
export const enhancedScreenshotService = new EnhancedScreenshotService() export const enhancedScreenshotService = new EnhancedScreenshotService()
export type { ScreenshotResult, ScreenshotOptions } export type { EnhancedScreenshotOptions }

View File

@@ -25,54 +25,18 @@ class PageRegistry {
description: "用户行为洞察分析", description: "用户行为洞察分析",
category: "用户分析", category: "用户分析",
}, },
{
path: "/user-discovery/behavior",
name: "行为分析",
description: "用户行为模式分析",
category: "用户分析",
},
{
path: "/user-discovery/segmentation",
name: "用户分群",
description: "智能用户分群",
category: "用户分析",
},
{ {
path: "/user-valuation", path: "/user-valuation",
name: "用户估值", name: "用户估值",
description: "用户价值评估模型", description: "用户价值评估模型",
category: "价值分析", category: "价值分析",
}, },
{
path: "/user-valuation/model",
name: "估值模型",
description: "RFM价值评估",
category: "价值分析",
},
{
path: "/user-valuation/upgrade-paths",
name: "升级路径",
description: "用户价值提升策略",
category: "价值分析",
},
{ {
path: "/ai-analysis", path: "/ai-analysis",
name: "AI分析", name: "AI分析",
description: "智能数据洞察", description: "智能数据洞察",
category: "AI功能", category: "AI功能",
}, },
{
path: "/ai-analysis/trends",
name: "趋势识别",
description: "AI趋势预测",
category: "AI功能",
},
{
path: "/ai-analysis/anomaly",
name: "异常检测",
description: "智能异常监控",
category: "AI功能",
},
{ {
path: "/devices", path: "/devices",
name: "设备管理", name: "设备管理",

View File

@@ -3,33 +3,33 @@
* 结合多种技术方案确保能够成功捕获页面截图 * 结合多种技术方案确保能够成功捕获页面截图
*/ */
interface ScreenshotResult { export interface ScreenshotOptions {
format?: "png" | "jpeg" | "webp"
quality?: number
scale?: number
backgroundColor?: string
timeout?: number
width?: number
height?: number
}
export interface ScreenshotResult {
success: boolean success: boolean
dataUrl?: string dataUrl?: string
error?: string error?: string
method?: string method?: string
} }
interface ScreenshotOptions {
format?: "png" | "jpeg" | "webp"
quality?: number
scale?: number
width?: number
height?: number
backgroundColor?: string
timeout?: number
}
class ScreenshotService { class ScreenshotService {
async captureViewport(options: ScreenshotOptions = {}): Promise<ScreenshotResult> { async captureViewport(options: ScreenshotOptions = {}): Promise<ScreenshotResult> {
try { try {
// 方法1: 使用 html2canvas // 方法1: 使用 html2canvas
if (typeof window !== "undefined" && window.html2canvas) { if (typeof window !== "undefined" && window.html2canvas) {
const canvas = await window.html2canvas(document.body, { const canvas = await window.html2canvas(document.body, {
width: options.width || window.innerWidth,
height: options.height || window.innerHeight,
scale: options.scale || 1,
backgroundColor: options.backgroundColor || "#ffffff", backgroundColor: options.backgroundColor || "#ffffff",
scale: options.scale || 1,
width: options.width,
height: options.height,
useCORS: true, useCORS: true,
allowTaint: true, allowTaint: true,
}) })
@@ -46,8 +46,9 @@ class ScreenshotService {
// 方法2: 使用 dom-to-image // 方法2: 使用 dom-to-image
if (typeof window !== "undefined" && window.domtoimage) { if (typeof window !== "undefined" && window.domtoimage) {
const dataUrl = await window.domtoimage.toPng(document.body, { const dataUrl = await window.domtoimage.toPng(document.body, {
width: options.width || window.innerWidth, bgcolor: options.backgroundColor || "#ffffff",
height: options.height || window.innerHeight, width: options.width,
height: options.height,
style: { style: {
transform: `scale(${options.scale || 1})`, transform: `scale(${options.scale || 1})`,
transformOrigin: "top left", transformOrigin: "top left",
@@ -63,32 +64,35 @@ class ScreenshotService {
// 方法3: 使用 Canvas API (基础实现) // 方法3: 使用 Canvas API (基础实现)
const canvas = document.createElement("canvas") const canvas = document.createElement("canvas")
canvas.width = options.width || window.innerWidth
canvas.height = options.height || window.innerHeight
const ctx = canvas.getContext("2d") const ctx = canvas.getContext("2d")
if (ctx) { if (!ctx) {
ctx.fillStyle = options.backgroundColor || "#ffffff" throw new Error("无法获取Canvas上下文")
ctx.fillRect(0, 0, canvas.width, canvas.height)
// 简单的文本渲染作为占位符
ctx.fillStyle = "#333333"
ctx.font = "16px Arial"
ctx.fillText("页面截图占位符", 50, 50)
ctx.fillText(`页面: ${window.location.pathname}`, 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",
}
} }
throw new Error("无法创建截图") 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) { } catch (error) {
console.error("截图失败:", error)
return { return {
success: false, success: false,
error: error instanceof Error ? error.message : "未知错误", error: error instanceof Error ? error.message : "未知错误",
@@ -101,10 +105,10 @@ class ScreenshotService {
// 使用 html2canvas 截取特定元素 // 使用 html2canvas 截取特定元素
if (typeof window !== "undefined" && window.html2canvas) { if (typeof window !== "undefined" && window.html2canvas) {
const canvas = await window.html2canvas(element, { const canvas = await window.html2canvas(element, {
width: options.width || element.offsetWidth,
height: options.height || element.offsetHeight,
scale: options.scale || 1,
backgroundColor: options.backgroundColor || "#ffffff", backgroundColor: options.backgroundColor || "#ffffff",
scale: options.scale || 1,
width: options.width,
height: options.height,
useCORS: true, useCORS: true,
allowTaint: true, allowTaint: true,
}) })
@@ -121,8 +125,9 @@ class ScreenshotService {
// 使用 dom-to-image 截取特定元素 // 使用 dom-to-image 截取特定元素
if (typeof window !== "undefined" && window.domtoimage) { if (typeof window !== "undefined" && window.domtoimage) {
const dataUrl = await window.domtoimage.toPng(element, { const dataUrl = await window.domtoimage.toPng(element, {
width: options.width || element.offsetWidth, bgcolor: options.backgroundColor || "#ffffff",
height: options.height || element.offsetHeight, width: options.width,
height: options.height,
}) })
return { return {
@@ -132,8 +137,9 @@ class ScreenshotService {
} }
} }
throw new Error("无法截取元素") throw new Error("没有可用的截图库")
} catch (error) { } catch (error) {
console.error("元素截图失败:", error)
return { return {
success: false, success: false,
error: error instanceof Error ? error.message : "未知错误", error: error instanceof Error ? error.message : "未知错误",
@@ -143,7 +149,14 @@ class ScreenshotService {
} }
export const screenshotService = new ScreenshotService() export const screenshotService = new ScreenshotService()
export type { ScreenshotResult, ScreenshotOptions }
// 扩展 Window 接口以包含截图库
declare global {
interface Window {
html2canvas?: any
domtoimage?: any
}
}
// 动态导入所需库 // 动态导入所需库
let htmlToImageModule: any = null let htmlToImageModule: any = null
@@ -369,18 +382,18 @@ async function captureWithCanvas(target: HTMLIFrameElement | Window): Promise<st
// 将文档转换为SVG数据URL // 将文档转换为SVG数据URL
const data = new XMLSerializer().serializeToString(document.documentElement) const data = new XMLSerializer().serializeToString(document.documentElement)
const svg = new Blob([data], { type: "image/svg+xml" }) const svg = new Blob([data], { type: "image/svg+xml" })
const url = URL.createObjectURL(svg) const svgUrl = URL.createObjectURL(svg)
// 等待图像加载 // 等待图像加载
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
img.onload = resolve img.onload = resolve
img.onerror = reject img.onerror = reject
img.src = url img.src = svgUrl
}) })
// 绘制图像 // 绘制图像
ctx.drawImage(img, 0, 0, width, height) ctx.drawImage(img, 0, 0, width, height)
URL.revokeObjectURL(url) URL.revokeObjectURL(svgUrl)
// 返回数据URL // 返回数据URL
return canvas.toDataURL("image/png") return canvas.toDataURL("image/png")

View File

@@ -32,7 +32,6 @@
"@radix-ui/react-tooltip": "latest", "@radix-ui/react-tooltip": "latest",
"@tanstack/react-table": "latest", "@tanstack/react-table": "latest",
"@tanstack/react-virtual": "latest", "@tanstack/react-virtual": "latest",
"buffer": "latest",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "latest", "cmdk": "latest",

27
pnpm-lock.yaml generated
View File

@@ -77,9 +77,6 @@ importers:
'@tanstack/react-virtual': '@tanstack/react-virtual':
specifier: latest specifier: latest
version: 3.13.12(react-dom@18.0.0(react@18.0.0))(react@18.0.0) version: 3.13.12(react-dom@18.0.0(react@18.0.0))(react@18.0.0)
buffer:
specifier: latest
version: 6.0.3
class-variance-authority: class-variance-authority:
specifier: ^0.7.1 specifier: ^0.7.1
version: 0.7.1 version: 0.7.1
@@ -1303,9 +1300,6 @@ packages:
balanced-match@1.0.2: balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
binary-extensions@2.3.0: binary-extensions@2.3.0:
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -1325,9 +1319,6 @@ packages:
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true hasBin: true
buffer@6.0.3:
resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
busboy@1.6.0: busboy@1.6.0:
resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==}
engines: {node: '>=10.16.0'} engines: {node: '>=10.16.0'}
@@ -2004,9 +1995,6 @@ packages:
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
ieee754@1.2.1:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
ignore@4.0.6: ignore@4.0.6:
resolution: {integrity: sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==} resolution: {integrity: sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==}
engines: {node: '>= 4'} engines: {node: '>= 4'}
@@ -4323,8 +4311,6 @@ snapshots:
balanced-match@1.0.2: {} balanced-match@1.0.2: {}
base64-js@1.5.1: {}
binary-extensions@2.3.0: {} binary-extensions@2.3.0: {}
brace-expansion@1.1.12: brace-expansion@1.1.12:
@@ -4347,11 +4333,6 @@ snapshots:
node-releases: 2.0.19 node-releases: 2.0.19
update-browserslist-db: 1.1.3(browserslist@4.25.1) update-browserslist-db: 1.1.3(browserslist@4.25.1)
buffer@6.0.3:
dependencies:
base64-js: 1.5.1
ieee754: 1.2.1
busboy@1.6.0: busboy@1.6.0:
dependencies: dependencies:
streamsearch: 1.1.0 streamsearch: 1.1.0
@@ -4834,7 +4815,7 @@ snapshots:
eslint: 8.0.0 eslint: 8.0.0
eslint-import-resolver-node: 0.3.9 eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(eslint@8.0.0))(eslint@8.0.0) eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(eslint@8.0.0))(eslint@8.0.0)
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.0.0)(typescript@5.0.2))(eslint-import-resolver-typescript@3.10.1)(eslint@8.0.0) eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.0.0)(typescript@5.0.2))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@8.0.0))(eslint@8.0.0))(eslint@8.0.0)
eslint-plugin-jsx-a11y: 6.10.2(eslint@8.0.0) eslint-plugin-jsx-a11y: 6.10.2(eslint@8.0.0)
eslint-plugin-react: 7.37.5(eslint@8.0.0) eslint-plugin-react: 7.37.5(eslint@8.0.0)
eslint-plugin-react-hooks: 5.0.0-canary-7118f5dd7-20230705(eslint@8.0.0) eslint-plugin-react-hooks: 5.0.0-canary-7118f5dd7-20230705(eslint@8.0.0)
@@ -4864,7 +4845,7 @@ snapshots:
tinyglobby: 0.2.14 tinyglobby: 0.2.14
unrs-resolver: 1.11.1 unrs-resolver: 1.11.1
optionalDependencies: optionalDependencies:
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.0.0)(typescript@5.0.2))(eslint-import-resolver-typescript@3.10.1)(eslint@8.0.0) eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.0.0)(typescript@5.0.2))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@8.0.0))(eslint@8.0.0))(eslint@8.0.0)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -4879,7 +4860,7 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.0.0)(typescript@5.0.2))(eslint-import-resolver-typescript@3.10.1)(eslint@8.0.0): eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.0.0)(typescript@5.0.2))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@8.0.0))(eslint@8.0.0))(eslint@8.0.0):
dependencies: dependencies:
'@rtsao/scc': 1.1.0 '@rtsao/scc': 1.1.0
array-includes: 3.1.9 array-includes: 3.1.9
@@ -5222,8 +5203,6 @@ snapshots:
dependencies: dependencies:
safer-buffer: 2.1.2 safer-buffer: 2.1.2
ieee754@1.2.1: {}
ignore@4.0.6: {} ignore@4.0.6: {}
ignore@5.3.2: {} ignore@5.3.2: {}