Files
users/app/documentation/page.tsx
v0 4ea8963ddc fix: resolve build errors and complete missing files
Fix CSS issues, add missing files, and optimize documentation page.

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

675 lines
24 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

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

"use client"
import { useState, useEffect, useRef } from "react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"
import { Checkbox } from "@/components/ui/checkbox"
import { Progress } from "@/components/ui/progress"
import { Badge } from "@/components/ui/badge"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { ScrollArea } from "@/components/ui/scroll-area"
import { toast } from "@/components/ui/use-toast"
import {
Camera,
Download,
FileText,
ImageIcon,
Settings,
Eye,
Trash2,
RefreshCw,
Monitor,
Smartphone,
Tablet,
Globe,
CheckCircle,
XCircle,
Clock,
Zap,
} from "lucide-react"
import {
enhancedScreenshotService,
type EnhancedScreenshotOptions,
} from "@/lib/documentation/enhanced-screenshot-service"
import { generateDocx } from "@/lib/documentation/docx-generator"
import { pageRegistry } from "@/lib/documentation/page-registry"
interface Screenshot {
id: string
name: string
url: string
dataUrl?: string
timestamp: Date
status: "pending" | "success" | "failed" | "capturing"
error?: string
method?: string
size?: { width: number; height: number }
}
interface DocumentSettings {
title: string
author: string
description: string
includeTimestamp: boolean
includePageUrls: boolean
imageFormat: "png" | "jpeg" | "webp"
imageQuality: number
pageSize: "A4" | "A3" | "Letter"
orientation: "portrait" | "landscape"
}
export default function DocumentationPage() {
const [screenshots, setScreenshots] = useState<Screenshot[]>([])
const [selectedPages, setSelectedPages] = useState<string[]>([])
const [isCapturing, setIsCapturing] = useState(false)
const [captureProgress, setCaptureProgress] = useState(0)
const [isGeneratingDoc, setIsGeneratingDoc] = useState(false)
const [previewMode, setPreviewMode] = useState<"desktop" | "tablet" | "mobile">("desktop")
const [currentPreviewUrl, setCurrentPreviewUrl] = useState("")
const [documentSettings, setDocumentSettings] = useState<DocumentSettings>({
title: "用户数据资产中台文档",
author: "系统管理员",
description: "自动生成的系统文档",
includeTimestamp: true,
includePageUrls: true,
imageFormat: "png",
imageQuality: 0.9,
pageSize: "A4",
orientation: "portrait",
})
const previewRef = useRef<HTMLIFrameElement>(null)
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))
}, [])
const handlePageSelection = (pagePath: string, checked: boolean) => {
if (checked) {
setSelectedPages((prev) => [...prev, pagePath])
} else {
setSelectedPages((prev) => prev.filter((p) => p !== pagePath))
}
}
const selectAllPages = () => {
setSelectedPages(pages.map((p) => p.path))
}
const deselectAllPages = () => {
setSelectedPages([])
}
const getViewportSize = () => {
switch (previewMode) {
case "mobile":
return { width: 375, height: 667 }
case "tablet":
return { width: 768, height: 1024 }
default:
return { width: 1920, height: 1080 }
}
}
const captureScreenshot = async (page: (typeof pages)[0]): Promise<Screenshot> => {
const screenshot: Screenshot = {
id: `${page.path}-${Date.now()}`,
name: page.name,
url: page.path,
timestamp: new Date(),
status: "capturing",
}
try {
const result = await enhancedScreenshotService.captureViewport({
...screenshotOptions,
...getViewportSize(),
watermarkText: `${page.name} - ${new Date().toLocaleString()}`,
})
if (result.success && result.dataUrl) {
return {
...screenshot,
status: "success",
dataUrl: result.dataUrl,
method: result.method,
size: getViewportSize(),
}
} else {
throw new Error(result.error || "截图失败")
}
} catch (error) {
console.error(`截图失败 ${page.name}:`, error)
return {
...screenshot,
status: "failed",
error: error instanceof Error ? error.message : "未知错误",
}
}
}
const startCapture = async () => {
if (selectedPages.length === 0) {
toast({
title: "请选择页面",
description: "请至少选择一个页面进行截图",
variant: "destructive",
})
return
}
setIsCapturing(true)
setCaptureProgress(0)
setScreenshots([])
const selectedPageObjects = pages.filter((p) => selectedPages.includes(p.path))
const newScreenshots: Screenshot[] = []
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,
url: page.path,
timestamp: new Date(),
status: "capturing",
}
newScreenshots.push(pendingScreenshot)
setScreenshots([...newScreenshots])
try {
const screenshot = await captureScreenshot(page)
const updatedScreenshots = [...newScreenshots]
updatedScreenshots[i] = screenshot
setScreenshots(updatedScreenshots)
newScreenshots[i] = screenshot
if (screenshot.status === "success") {
toast({
title: "截图成功",
description: `${page.name} 截图完成`,
})
} else {
toast({
title: "截图失败",
description: `${page.name}: ${screenshot.error}`,
variant: "destructive",
})
}
} catch (error) {
console.error(`截图过程出错:`, error)
const failedScreenshot: Screenshot = {
...pendingScreenshot,
status: "failed",
error: error instanceof Error ? error.message : "未知错误",
}
const updatedScreenshots = [...newScreenshots]
updatedScreenshots[i] = failedScreenshot
setScreenshots(updatedScreenshots)
newScreenshots[i] = failedScreenshot
}
if (i < selectedPageObjects.length - 1) {
await new Promise((resolve) => setTimeout(resolve, 1000))
}
}
setCaptureProgress(100)
setIsCapturing(false)
const successCount = newScreenshots.filter((s) => s.status === "success").length
const failedCount = newScreenshots.filter((s) => s.status === "failed").length
toast({
title: "截图完成",
description: `成功: ${successCount}, 失败: ${failedCount}`,
variant: successCount > 0 ? "default" : "destructive",
})
}
const generateDocument = async () => {
const successfulScreenshots = screenshots.filter((s) => s.status === "success" && s.dataUrl)
if (successfulScreenshots.length === 0) {
toast({
title: "没有可用的截图",
description: "请先成功截图至少一个页面",
variant: "destructive",
})
return
}
setIsGeneratingDoc(true)
try {
const docBuffer = await generateDocx(successfulScreenshots, documentSettings)
const blob = new Blob([docBuffer], {
type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
})
const url = URL.createObjectURL(blob)
const a = document.createElement("a")
a.href = url
a.download = `${documentSettings.title.replace(/[^a-zA-Z0-9\u4e00-\u9fa5]/g, "_")}_${new Date().toISOString().split("T")[0]}.docx`
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
toast({
title: "文档生成成功",
description: `已生成包含 ${successfulScreenshots.length} 个截图的文档`,
})
} catch (error) {
console.error("文档生成失败:", error)
toast({
title: "文档生成失败",
description: error instanceof Error ? error.message : "未知错误",
variant: "destructive",
})
} finally {
setIsGeneratingDoc(false)
}
}
const deleteScreenshot = (id: string) => {
setScreenshots((prev) => prev.filter((s) => s.id !== id))
toast({
title: "截图已删除",
description: "截图已从列表中移除",
})
}
const clearAllScreenshots = () => {
setScreenshots([])
toast({
title: "已清空所有截图",
description: "所有截图已从列表中移除",
})
}
const previewPage = (url: string) => {
setCurrentPreviewUrl(url)
}
const getStatusIcon = (status: Screenshot["status"]) => {
switch (status) {
case "success":
return <CheckCircle className="h-4 w-4 text-green-500" />
case "failed":
return <XCircle className="h-4 w-4 text-red-500" />
case "capturing":
return <Clock className="h-4 w-4 text-blue-500 animate-spin" />
default:
return <Clock className="h-4 w-4 text-gray-400" />
}
}
const getPreviewIcon = () => {
switch (previewMode) {
case "mobile":
return <Smartphone className="h-4 w-4" />
case "tablet":
return <Tablet className="h-4 w-4" />
default:
return <Monitor className="h-4 w-4" />
}
}
return (
<div className="container mx-auto p-6 space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold"></h1>
<p className="text-muted-foreground mt-1"></p>
</div>
<div className="flex items-center space-x-2">
<Badge variant="outline" className="flex items-center space-x-1">
<Zap className="h-3 w-3" />
<span></span>
</Badge>
</div>
</div>
<Tabs defaultValue="capture" className="space-y-6">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="capture"></TabsTrigger>
<TabsTrigger value="screenshots"></TabsTrigger>
<TabsTrigger value="settings"></TabsTrigger>
</TabsList>
<TabsContent value="capture" className="space-y-6">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2">
<Card>
<CardHeader>
<CardTitle className="flex items-center space-x-2">
<Globe className="h-5 w-5" />
<span></span>
</CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<Button variant="outline" size="sm" onClick={selectAllPages}>
</Button>
<Button variant="outline" size="sm" onClick={deselectAllPages}>
</Button>
</div>
<Badge variant="secondary">
{selectedPages.length} / {pages.length}
</Badge>
</div>
<ScrollArea className="h-[400px]">
<div className="space-y-2">
{pages.map((page) => (
<div
key={page.path}
className="flex items-center space-x-3 p-3 border rounded-lg hover:bg-gray-50"
>
<Checkbox
checked={selectedPages.includes(page.path)}
onCheckedChange={(checked) => handlePageSelection(page.path, checked as boolean)}
/>
<div className="flex-1 min-w-0">
<div className="font-medium">{page.name}</div>
<div className="text-sm text-muted-foreground truncate">{page.path}</div>
{page.description && (
<div className="text-xs text-muted-foreground mt-1">{page.description}</div>
)}
</div>
<Button variant="ghost" size="sm" onClick={() => previewPage(page.path)}>
<Eye className="h-4 w-4" />
</Button>
</div>
))}
</div>
</ScrollArea>
</CardContent>
</Card>
</div>
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center space-x-2">
<Camera className="h-5 w-5" />
<span></span>
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{isCapturing && (
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span></span>
<span>{Math.round(captureProgress)}%</span>
</div>
<Progress value={captureProgress} />
</div>
)}
<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>
<div className="text-sm text-muted-foreground">
<div className="flex items-center justify-between">
<span>:</span>
<span>{pages.length}</span>
</div>
<div className="flex items-center justify-between">
<span>:</span>
<span>{selectedPages.length}</span>
</div>
<div className="flex items-center justify-between">
<span>:</span>
<span>{screenshots.length}</span>
</div>
</div>
</CardContent>
</Card>
</div>
</div>
</TabsContent>
<TabsContent value="screenshots" className="space-y-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<ImageIcon className="h-5 w-5" />
<span></span>
</div>
<div className="flex items-center space-x-2">
<Badge variant="outline">{screenshots.filter((s) => s.status === "success").length} </Badge>
<Badge variant="destructive">{screenshots.filter((s) => s.status === "failed").length} </Badge>
{screenshots.length > 0 && (
<Button variant="outline" size="sm" onClick={clearAllScreenshots}>
<Trash2 className="h-4 w-4 mr-1" />
</Button>
)}
</div>
</CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
{screenshots.length === 0 ? (
<div className="text-center py-12 text-muted-foreground">
<ImageIcon className="h-12 w-12 mx-auto mb-4 opacity-50" />
<p></p>
<p className="text-sm">"页面截图"</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{screenshots.map((screenshot) => (
<Card key={screenshot.id} className="overflow-hidden">
<div className="aspect-video bg-gray-100 relative">
{screenshot.dataUrl ? (
<img
src={screenshot.dataUrl || "/placeholder.svg"}
alt={screenshot.name}
className="w-full h-full object-cover"
/>
) : (
<div className="flex items-center justify-center h-full">
{screenshot.status === "capturing" ? (
<RefreshCw className="h-8 w-8 animate-spin text-blue-500" />
) : (
<ImageIcon className="h-8 w-8 text-gray-400" />
)}
</div>
)}
<div className="absolute top-2 right-2">{getStatusIcon(screenshot.status)}</div>
</div>
<CardContent className="p-4">
<div className="space-y-2">
<div className="font-medium truncate">{screenshot.name}</div>
<div className="text-sm text-muted-foreground truncate">{screenshot.url}</div>
{screenshot.method && (
<Badge variant="outline" className="text-xs">
{screenshot.method}
</Badge>
)}
{screenshot.error && <div className="text-xs text-red-500 truncate">{screenshot.error}</div>}
<div className="text-xs text-muted-foreground">{screenshot.timestamp.toLocaleString()}</div>
<div className="flex items-center justify-between">
<div className="text-xs text-muted-foreground">
{screenshot.size && `${screenshot.size.width}×${screenshot.size.height}`}
</div>
<Button variant="ghost" size="sm" onClick={() => deleteScreenshot(screenshot.id)}>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
</CardContent>
</Card>
))}
</div>
)}
</CardContent>
</Card>
{screenshots.some((s) => s.status === "success") && (
<Card>
<CardHeader>
<CardTitle className="flex items-center space-x-2">
<FileText className="h-5 w-5" />
<span></span>
</CardTitle>
<CardDescription>Word文档</CardDescription>
</CardHeader>
<CardContent>
<Button onClick={generateDocument} disabled={isGeneratingDoc} className="w-full">
{isGeneratingDoc ? (
<>
<RefreshCw className="mr-2 h-4 w-4 animate-spin" />
...
</>
) : (
<>
<Download className="mr-2 h-4 w-4" />
Word文档
</>
)}
</Button>
</CardContent>
</Card>
)}
</TabsContent>
<TabsContent value="settings" className="space-y-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center space-x-2">
<Settings className="h-5 w-5" />
<span></span>
</CardTitle>
<CardDescription>Word文档格式和内容</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-4">
<div className="space-y-2">
<label className="text-sm font-medium"></label>
<Input
value={documentSettings.title}
onChange={(e) =>
setDocumentSettings((prev) => ({
...prev,
title: e.target.value,
}))
}
placeholder="输入文档标题"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium"></label>
<Input
value={documentSettings.author}
onChange={(e) =>
setDocumentSettings((prev) => ({
...prev,
author: e.target.value,
}))
}
placeholder="输入作者姓名"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium"></label>
<Textarea
value={documentSettings.description}
onChange={(e) =>
setDocumentSettings((prev) => ({
...prev,
description: e.target.value,
}))
}
placeholder="输入文档描述"
rows={3}
/>
</div>
</div>
<div className="space-y-4">
<div className="space-y-3">
<label className="text-sm font-medium"></label>
<div className="space-y-2">
<div className="flex items-center space-x-2">
<Checkbox
checked={documentSettings.includeTimestamp}
onCheckedChange={(checked) =>
setDocumentSettings((prev) => ({
...prev,
includeTimestamp: checked as boolean,
}))
}
/>
<label className="text-sm"></label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
checked={documentSettings.includePageUrls}
onCheckedChange={(checked) =>
setDocumentSettings((prev) => ({
...prev,
includePageUrls: checked as boolean,
}))
}
/>
<label className="text-sm">URL</label>
</div>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
)
}