Redesign navigation, home overview, user portrait, and valuation pages with improved functionality and responsive design. Co-authored-by: null <4804959+fnvtk@users.noreply.github.com>
1052 lines
38 KiB
TypeScript
1052 lines
38 KiB
TypeScript
"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 { 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"
|
||
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 ScreenshotOptions } 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<ScreenshotOptions>({
|
||
format: "png",
|
||
quality: 0.9,
|
||
scale: 1,
|
||
backgroundColor: "#ffffff",
|
||
timeout: 30000,
|
||
})
|
||
|
||
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 {
|
||
// 方法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()
|
||
})
|
||
|
||
// 尝试截图新窗口
|
||
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 {
|
||
...screenshot,
|
||
status: "success",
|
||
dataUrl: result.dataUrl,
|
||
method: result.method,
|
||
size: getViewportSize(),
|
||
}
|
||
}
|
||
|
||
// 方法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 {
|
||
...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 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)
|
||
|
||
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-4">
|
||
<TabsTrigger value="capture">页面截图</TabsTrigger>
|
||
<TabsTrigger value="preview">预览页面</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">
|
||
<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">
|
||
<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>
|
||
)}
|
||
|
||
<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>
|
||
)}
|
||
</div>
|
||
|
||
<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="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>
|
||
<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-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">
|
||
<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>
|
||
)
|
||
}
|