177 lines
5.7 KiB
TypeScript
177 lines
5.7 KiB
TypeScript
|
|
"use client"
|
|||
|
|
|
|||
|
|
import { useState, useEffect } from "react"
|
|||
|
|
import { Button } from "@/components/ui/button"
|
|||
|
|
import { Card, CardContent } from "@/components/ui/card"
|
|||
|
|
import { Textarea } from "@/components/ui/textarea"
|
|||
|
|
import { Label } from "@/components/ui/label"
|
|||
|
|
import { Alert, AlertDescription } from "@/components/ui/alert"
|
|||
|
|
import { Loader2, FileText, AlertTriangle } from "lucide-react"
|
|||
|
|
import { generateDocx } from "@/lib/documentation/docx-generator"
|
|||
|
|
|
|||
|
|
interface DocumentSection {
|
|||
|
|
title: string
|
|||
|
|
path: string
|
|||
|
|
description: string
|
|||
|
|
screenshot: string
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
interface DocumentGeneratorProps {
|
|||
|
|
pages: Array<{ path: string; title: string; description: string }>
|
|||
|
|
screenshots: Record<string, string>
|
|||
|
|
onDocumentGenerated: (url: string) => void
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export default function DocumentGenerator({ pages, screenshots, onDocumentGenerated }: DocumentGeneratorProps) {
|
|||
|
|
const [isGenerating, setIsGenerating] = useState(false)
|
|||
|
|
const [error, setError] = useState<string | null>(null)
|
|||
|
|
const [sections, setSections] = useState<DocumentSection[]>([])
|
|||
|
|
const [descriptions, setDescriptions] = useState<Record<string, string>>({})
|
|||
|
|
|
|||
|
|
// 初始化页面描述
|
|||
|
|
useEffect(() => {
|
|||
|
|
const initialDescriptions: Record<string, string> = {}
|
|||
|
|
pages.forEach((page) => {
|
|||
|
|
initialDescriptions[page.path] = page.description || ""
|
|||
|
|
})
|
|||
|
|
setDescriptions(initialDescriptions)
|
|||
|
|
}, [pages])
|
|||
|
|
|
|||
|
|
// 更新页面描述
|
|||
|
|
const updateDescription = (path: string, description: string) => {
|
|||
|
|
setDescriptions((prev) => ({
|
|||
|
|
...prev,
|
|||
|
|
[path]: description,
|
|||
|
|
}))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 准备文档部分
|
|||
|
|
useEffect(() => {
|
|||
|
|
const newSections = pages
|
|||
|
|
.filter((page) => screenshots[page.path])
|
|||
|
|
.map((page) => ({
|
|||
|
|
title: page.title,
|
|||
|
|
path: page.path,
|
|||
|
|
description: descriptions[page.path] || page.description || "",
|
|||
|
|
screenshot: screenshots[page.path],
|
|||
|
|
}))
|
|||
|
|
|
|||
|
|
setSections(newSections)
|
|||
|
|
}, [pages, screenshots, descriptions])
|
|||
|
|
|
|||
|
|
// 生成文档
|
|||
|
|
const generateDocument = async () => {
|
|||
|
|
if (sections.length === 0) {
|
|||
|
|
setError("没有可用的截图,请先捕获页面截图")
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
setIsGenerating(true)
|
|||
|
|
setError(null)
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
console.log("开始生成文档...")
|
|||
|
|
console.log("文档部分数量:", sections.length)
|
|||
|
|
|
|||
|
|
// 准备文档数据
|
|||
|
|
const documentData = {
|
|||
|
|
title: "用户数据资产中台使用手册",
|
|||
|
|
author: "系统管理员",
|
|||
|
|
date: new Date().toLocaleDateString("zh-CN"),
|
|||
|
|
sections: sections,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
console.log("文档数据准备完成")
|
|||
|
|
|
|||
|
|
// 生成Word文档
|
|||
|
|
const docxUrl = await generateDocx(documentData)
|
|||
|
|
console.log("文档生成成功:", docxUrl)
|
|||
|
|
|
|||
|
|
// 调用回调函数
|
|||
|
|
onDocumentGenerated(docxUrl)
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error("生成文档时出错:", error)
|
|||
|
|
setError(`生成文档时出错: ${error instanceof Error ? error.message : String(error)}`)
|
|||
|
|
} finally {
|
|||
|
|
setIsGenerating(false)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="space-y-6">
|
|||
|
|
<div className="flex justify-between items-center">
|
|||
|
|
<h2 className="text-xl font-semibold">文档内容编辑</h2>
|
|||
|
|
<Button
|
|||
|
|
onClick={generateDocument}
|
|||
|
|
disabled={isGenerating || sections.length === 0}
|
|||
|
|
className="flex items-center gap-2"
|
|||
|
|
>
|
|||
|
|
{isGenerating ? (
|
|||
|
|
<>
|
|||
|
|
<Loader2 className="h-4 w-4 animate-spin" />
|
|||
|
|
生成中...
|
|||
|
|
</>
|
|||
|
|
) : (
|
|||
|
|
<>
|
|||
|
|
<FileText className="h-4 w-4" />
|
|||
|
|
生成文档
|
|||
|
|
</>
|
|||
|
|
)}
|
|||
|
|
</Button>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{error && (
|
|||
|
|
<Alert variant="destructive">
|
|||
|
|
<AlertTriangle className="h-4 w-4" />
|
|||
|
|
<AlertDescription>{error}</AlertDescription>
|
|||
|
|
</Alert>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{sections.length === 0 ? (
|
|||
|
|
<div className="text-center py-8">
|
|||
|
|
<p className="text-gray-500">没有可用的截图</p>
|
|||
|
|
<p className="text-sm text-gray-400 mt-1">请先捕获页面截图</p>
|
|||
|
|
</div>
|
|||
|
|
) : (
|
|||
|
|
<div className="space-y-4">
|
|||
|
|
<p className="text-sm text-gray-500">
|
|||
|
|
已捕获 {sections.length} 个页面的截图。您可以编辑每个页面的描述,然后生成文档。
|
|||
|
|
</p>
|
|||
|
|
|
|||
|
|
{sections.map((section) => (
|
|||
|
|
<Card key={section.path} className="overflow-hidden">
|
|||
|
|
<CardContent className="p-4">
|
|||
|
|
<div className="flex flex-col md:flex-row gap-4">
|
|||
|
|
<div className="md:w-1/3">
|
|||
|
|
<div className="border rounded-md overflow-hidden">
|
|||
|
|
<img
|
|||
|
|
src={section.screenshot || "/placeholder.svg"}
|
|||
|
|
alt={section.title}
|
|||
|
|
className="w-full h-auto object-contain"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
<p className="text-sm font-medium mt-2">{section.title}</p>
|
|||
|
|
<p className="text-xs text-gray-500">{section.path}</p>
|
|||
|
|
</div>
|
|||
|
|
<div className="md:w-2/3">
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
<Label htmlFor={`desc-${section.path}`}>页面描述</Label>
|
|||
|
|
<Textarea
|
|||
|
|
id={`desc-${section.path}`}
|
|||
|
|
value={descriptions[section.path] || ""}
|
|||
|
|
onChange={(e) => updateDescription(section.path, e.target.value)}
|
|||
|
|
placeholder="输入此页面的详细描述"
|
|||
|
|
rows={6}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</CardContent>
|
|||
|
|
</Card>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
)
|
|||
|
|
}
|