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 { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Checkbox } from "@/components/ui/checkbox"
import { Progress } from "@/components/ui/progress"
import { Badge } from "@/components/ui/badge"
@@ -30,7 +29,10 @@ import {
Clock,
Zap,
} 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 { pageRegistry } from "@/lib/documentation/page-registry"
@@ -79,18 +81,20 @@ export default function DocumentationPage() {
})
const previewRef = useRef<HTMLIFrameElement>(null)
const [screenshotOptions, setScreenshotOptions] = useState<ScreenshotOptions>({
const [screenshotOptions, setScreenshotOptions] = useState<EnhancedScreenshotOptions>({
format: "png",
quality: 0.9,
scale: 1,
backgroundColor: "#ffffff",
timeout: 30000,
waitForImages: true,
waitForFonts: true,
addWatermark: true,
})
const pages = pageRegistry.getAllPages()
useEffect(() => {
// 初始化选中所有页面
setSelectedPages(pages.map((p) => p.path))
}, [])
@@ -131,80 +135,13 @@ export default function DocumentationPage() {
}
try {
// 方法1: 尝试直接截图当前页面(如果是当前页面)
if (window.location.pathname === page.path) {
const result = await enhancedScreenshotService.captureViewport({
...screenshotOptions,
...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()
const result = await enhancedScreenshotService.captureViewport({
...screenshotOptions,
...getViewportSize(),
watermarkText: `${page.name} - ${new Date().toLocaleString()}`,
})
// 尝试截图新窗口
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) {
if (result.success && result.dataUrl) {
return {
...screenshot,
status: "success",
@@ -212,52 +149,9 @@ export default function DocumentationPage() {
method: result.method,
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) {
console.error(`截图失败 ${page.name}:`, error)
return {
@@ -288,10 +182,8 @@ export default function DocumentationPage() {
for (let i = 0; i < selectedPageObjects.length; i++) {
const page = selectedPageObjects[i]
// 更新进度
setCaptureProgress((i / selectedPageObjects.length) * 100)
// 添加待处理的截图
const pendingScreenshot: Screenshot = {
id: `${page.path}-${Date.now()}`,
name: page.name,
@@ -306,7 +198,6 @@ export default function DocumentationPage() {
try {
const screenshot = await captureScreenshot(page)
// 更新截图状态
const updatedScreenshots = [...newScreenshots]
updatedScreenshots[i] = screenshot
setScreenshots(updatedScreenshots)
@@ -338,7 +229,6 @@ export default function DocumentationPage() {
newScreenshots[i] = failedScreenshot
}
// 添加延迟避免过快请求
if (i < selectedPageObjects.length - 1) {
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 successfulScreenshots = screenshots.filter((s) => s.status === "success" && s.dataUrl)
@@ -430,7 +264,6 @@ export default function DocumentationPage() {
try {
const docBuffer = await generateDocx(successfulScreenshots, documentSettings)
// 下载文档
const blob = new Blob([docBuffer], {
type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
})
@@ -520,9 +353,8 @@ export default function DocumentationPage() {
</div>
<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="preview"></TabsTrigger>
<TabsTrigger value="screenshots"></TabsTrigger>
<TabsTrigger value="settings"></TabsTrigger>
</TabsList>
@@ -583,105 +415,6 @@ export default function DocumentationPage() {
</div>
<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>
<CardHeader>
<CardTitle className="flex items-center space-x-2">
@@ -700,37 +433,23 @@ export default function DocumentationPage() {
</div>
)}
<div className="space-y-2">
<Button
onClick={startCapture}
disabled={isCapturing || selectedPages.length === 0}
className="w-full"
>
{isCapturing ? (
<>
<RefreshCw className="mr-2 h-4 w-4 animate-spin" />
...
</>
) : (
<>
<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>
<Button
onClick={startCapture}
disabled={isCapturing || selectedPages.length === 0}
className="w-full"
>
{isCapturing ? (
<>
<RefreshCw className="mr-2 h-4 w-4 animate-spin" />
...
</>
) : (
<>
<Camera className="mr-2 h-4 w-4" />
</>
)}
</div>
</Button>
<div className="text-sm text-muted-foreground">
<div className="flex items-center justify-between">
@@ -752,65 +471,6 @@ export default function DocumentationPage() {
</div>
</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">
<Card>
<CardHeader>
@@ -974,43 +634,6 @@ export default function DocumentationPage() {
</div>
<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">
<label className="text-sm font-medium"></label>
<div className="space-y-2">