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:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user