307 lines
12 KiB
TypeScript
307 lines
12 KiB
TypeScript
|
|
"use client"
|
|||
|
|
|
|||
|
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
|||
|
|
import { getDocumentationCatalog, type DocumentationPage } from "@/lib/documentation/catalog"
|
|||
|
|
import { FileText, Download, Loader2, CheckCircle, XCircle, Eye, RefreshCw } from "lucide-react"
|
|||
|
|
|
|||
|
|
type PageStatus = "pending" | "loading" | "success" | "error"
|
|||
|
|
|
|||
|
|
type PageState = {
|
|||
|
|
page: DocumentationPage
|
|||
|
|
status: PageStatus
|
|||
|
|
error?: string
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export default function DocumentationToolPage() {
|
|||
|
|
const [isGenerating, setIsGenerating] = useState(false)
|
|||
|
|
const [error, setError] = useState<string | null>(null)
|
|||
|
|
const [progress, setProgress] = useState(0)
|
|||
|
|
const [currentPage, setCurrentPage] = useState<string | null>(null)
|
|||
|
|
const [pageStates, setPageStates] = useState<PageState[]>([])
|
|||
|
|
const [previewPath, setPreviewPath] = useState<string | null>(null)
|
|||
|
|
const [showPreview, setShowPreview] = useState(false)
|
|||
|
|
const iframeRef = useRef<HTMLIFrameElement>(null)
|
|||
|
|
|
|||
|
|
const pages = useMemo(() => getDocumentationCatalog(), [])
|
|||
|
|
|
|||
|
|
const groupedPages = useMemo(() => {
|
|||
|
|
const groups: Record<string, DocumentationPage[]> = {}
|
|||
|
|
for (const page of pages) {
|
|||
|
|
if (!groups[page.group]) groups[page.group] = []
|
|||
|
|
groups[page.group].push(page)
|
|||
|
|
}
|
|||
|
|
return groups
|
|||
|
|
}, [pages])
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
setPageStates(pages.map((page) => ({ page, status: "pending" })))
|
|||
|
|
}, [pages])
|
|||
|
|
|
|||
|
|
const handleGenerate = async () => {
|
|||
|
|
setError(null)
|
|||
|
|
setIsGenerating(true)
|
|||
|
|
setProgress(0)
|
|||
|
|
setCurrentPage(null)
|
|||
|
|
setPageStates(pages.map((page) => ({ page, status: "loading" })))
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
// Simulate progress while waiting for the API
|
|||
|
|
let progressValue = 0
|
|||
|
|
const progressInterval = setInterval(() => {
|
|||
|
|
progressValue += 2
|
|||
|
|
const pageIndex = Math.floor((progressValue / 100) * pages.length)
|
|||
|
|
const nextPage = pages[Math.min(pageIndex, pages.length - 1)]
|
|||
|
|
if (nextPage) setCurrentPage(nextPage.title)
|
|||
|
|
setProgress(Math.min(progressValue, 90))
|
|||
|
|
|
|||
|
|
// Update page states to show progress
|
|||
|
|
setPageStates((prev) =>
|
|||
|
|
prev.map((s, idx) => ({
|
|||
|
|
...s,
|
|||
|
|
status: idx < pageIndex ? "success" : idx === pageIndex ? "loading" : "pending",
|
|||
|
|
})),
|
|||
|
|
)
|
|||
|
|
}, 800)
|
|||
|
|
|
|||
|
|
// Get token from URL if provided
|
|||
|
|
const urlParams = new URLSearchParams(window.location.search)
|
|||
|
|
const token = urlParams.get("token") || ""
|
|||
|
|
|
|||
|
|
const response = await fetch(`/api/documentation/generate${token ? `?token=${token}` : ""}`, {
|
|||
|
|
method: "POST",
|
|||
|
|
headers: {
|
|||
|
|
"Content-Type": "application/json",
|
|||
|
|
...(token ? { "x-documentation-token": token } : {}),
|
|||
|
|
},
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
clearInterval(progressInterval)
|
|||
|
|
|
|||
|
|
if (!response.ok) {
|
|||
|
|
const text = await response.text().catch(() => "")
|
|||
|
|
let errorMessage = `生成失败(${response.status})`
|
|||
|
|
try {
|
|||
|
|
const json = JSON.parse(text)
|
|||
|
|
errorMessage = json.error || errorMessage
|
|||
|
|
} catch {
|
|||
|
|
if (text) errorMessage = text
|
|||
|
|
}
|
|||
|
|
throw new Error(errorMessage)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
setProgress(100)
|
|||
|
|
setCurrentPage("完成")
|
|||
|
|
setPageStates(pages.map((page) => ({ page, status: "success" })))
|
|||
|
|
|
|||
|
|
const blob = await response.blob()
|
|||
|
|
const url = URL.createObjectURL(blob)
|
|||
|
|
const a = document.createElement("a")
|
|||
|
|
a.href = url
|
|||
|
|
a.download = `应用功能文档_${new Date().toISOString().slice(0, 10)}.docx`
|
|||
|
|
document.body.appendChild(a)
|
|||
|
|
a.click()
|
|||
|
|
a.remove()
|
|||
|
|
URL.revokeObjectURL(url)
|
|||
|
|
} catch (e) {
|
|||
|
|
const message = e instanceof Error ? e.message : String(e)
|
|||
|
|
setError(message)
|
|||
|
|
setPageStates((prev) => prev.map((s) => ({ ...s, status: "error" })))
|
|||
|
|
} finally {
|
|||
|
|
setIsGenerating(false)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const handlePreview = useCallback((path: string) => {
|
|||
|
|
setPreviewPath(path)
|
|||
|
|
setShowPreview(true)
|
|||
|
|
}, [])
|
|||
|
|
|
|||
|
|
const getStatusIcon = (status: PageStatus) => {
|
|||
|
|
switch (status) {
|
|||
|
|
case "pending":
|
|||
|
|
return <div className="w-4 h-4 rounded-full bg-gray-600" />
|
|||
|
|
case "loading":
|
|||
|
|
return <Loader2 className="w-4 h-4 animate-spin text-teal-400" />
|
|||
|
|
case "success":
|
|||
|
|
return <CheckCircle className="w-4 h-4 text-green-500" />
|
|||
|
|
case "error":
|
|||
|
|
return <XCircle className="w-4 h-4 text-red-500" />
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<main className="min-h-screen bg-background text-foreground p-4 pb-24">
|
|||
|
|
<div className="max-w-md mx-auto space-y-4">
|
|||
|
|
{/* Header */}
|
|||
|
|
<div className="flex items-center gap-3 py-2">
|
|||
|
|
<FileText className="w-6 h-6 text-teal-400" />
|
|||
|
|
<div>
|
|||
|
|
<h1 className="text-lg font-semibold">文档生成器</h1>
|
|||
|
|
<p className="text-xs text-muted-foreground">自动截图并导出专业文档</p>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Info Card */}
|
|||
|
|
<div className="bg-card border border-border rounded-xl p-4 space-y-2">
|
|||
|
|
<div className="flex items-center justify-between text-sm">
|
|||
|
|
<span className="text-muted-foreground">页面总数</span>
|
|||
|
|
<span className="font-medium text-teal-400">{pages.length} 个</span>
|
|||
|
|
</div>
|
|||
|
|
<div className="flex items-center justify-between text-sm">
|
|||
|
|
<span className="text-muted-foreground">分组数量</span>
|
|||
|
|
<span className="font-medium">{Object.keys(groupedPages).length} 组</span>
|
|||
|
|
</div>
|
|||
|
|
<div className="flex items-center justify-between text-sm">
|
|||
|
|
<span className="text-muted-foreground">输出格式</span>
|
|||
|
|
<span className="font-medium">Word (.docx)</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Progress */}
|
|||
|
|
{isGenerating && (
|
|||
|
|
<div className="bg-card border border-border rounded-xl p-4 space-y-3">
|
|||
|
|
<div className="flex items-center justify-between text-sm">
|
|||
|
|
<span className="text-muted-foreground">生成进度</span>
|
|||
|
|
<span className="font-medium text-teal-400">{Math.round(progress)}%</span>
|
|||
|
|
</div>
|
|||
|
|
<div className="w-full h-2 bg-muted rounded-full overflow-hidden">
|
|||
|
|
<div
|
|||
|
|
className="h-full bg-gradient-to-r from-teal-500 to-cyan-400 transition-all duration-300"
|
|||
|
|
style={{ width: `${progress}%` }}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
{currentPage && <p className="text-xs text-muted-foreground truncate">正在处理: {currentPage}</p>}
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* Error */}
|
|||
|
|
{error && (
|
|||
|
|
<div className="bg-destructive/10 border border-destructive/30 text-destructive rounded-xl p-3 text-sm">
|
|||
|
|
<p className="font-medium mb-1">生成失败</p>
|
|||
|
|
<p className="text-xs opacity-80">{error}</p>
|
|||
|
|
<p className="text-xs mt-2 opacity-60">提示: 如需授权,请在URL中添加 ?token=your_token</p>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* Generate Button */}
|
|||
|
|
<button
|
|||
|
|
type="button"
|
|||
|
|
onClick={handleGenerate}
|
|||
|
|
disabled={isGenerating}
|
|||
|
|
className="w-full bg-gradient-to-r from-teal-500 to-cyan-500 text-white rounded-xl py-3.5 font-medium disabled:opacity-60 flex items-center justify-center gap-2 shadow-lg shadow-teal-500/20"
|
|||
|
|
>
|
|||
|
|
{isGenerating ? (
|
|||
|
|
<>
|
|||
|
|
<Loader2 className="w-5 h-5 animate-spin" />
|
|||
|
|
<span>正在生成文档...</span>
|
|||
|
|
</>
|
|||
|
|
) : (
|
|||
|
|
<>
|
|||
|
|
<Download className="w-5 h-5" />
|
|||
|
|
<span>一键生成 Word 文档</span>
|
|||
|
|
</>
|
|||
|
|
)}
|
|||
|
|
</button>
|
|||
|
|
|
|||
|
|
{/* Page List */}
|
|||
|
|
<div className="space-y-3">
|
|||
|
|
<h2 className="text-sm font-medium text-muted-foreground flex items-center gap-2">
|
|||
|
|
<span>文档目录预览</span>
|
|||
|
|
<span className="text-xs opacity-60">({pages.length}个页面)</span>
|
|||
|
|
</h2>
|
|||
|
|
|
|||
|
|
{Object.entries(groupedPages).map(([group, groupPages]) => (
|
|||
|
|
<div key={group} className="bg-card border border-border rounded-xl overflow-hidden">
|
|||
|
|
<div className="px-3 py-2 bg-muted/50 border-b border-border">
|
|||
|
|
<h3 className="text-sm font-medium text-teal-400">{group}</h3>
|
|||
|
|
</div>
|
|||
|
|
<div className="divide-y divide-border">
|
|||
|
|
{groupPages.map((page, index) => {
|
|||
|
|
const state = pageStates.find((s) => s.page.path === page.path)
|
|||
|
|
return (
|
|||
|
|
<div
|
|||
|
|
key={page.path}
|
|||
|
|
className="px-3 py-2.5 flex items-center gap-3 hover:bg-muted/30 transition-colors"
|
|||
|
|
>
|
|||
|
|
<span className="text-xs text-muted-foreground w-5">{index + 1}</span>
|
|||
|
|
{state && getStatusIcon(state.status)}
|
|||
|
|
<div className="flex-1 min-w-0">
|
|||
|
|
<p className="text-sm font-medium truncate">{page.title}</p>
|
|||
|
|
{page.subtitle && <p className="text-xs text-muted-foreground truncate">{page.subtitle}</p>}
|
|||
|
|
</div>
|
|||
|
|
<button
|
|||
|
|
type="button"
|
|||
|
|
onClick={() => handlePreview(page.path)}
|
|||
|
|
className="p-1.5 text-muted-foreground hover:text-foreground hover:bg-muted rounded-lg transition-colors"
|
|||
|
|
title="预览页面"
|
|||
|
|
>
|
|||
|
|
<Eye className="w-4 h-4" />
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
)
|
|||
|
|
})}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Features */}
|
|||
|
|
<div className="bg-card border border-border rounded-xl p-4 space-y-3">
|
|||
|
|
<h3 className="text-sm font-medium">文档包含内容</h3>
|
|||
|
|
<ul className="text-xs text-muted-foreground space-y-2">
|
|||
|
|
<li className="flex items-start gap-2">
|
|||
|
|
<CheckCircle className="w-4 h-4 text-green-500 flex-shrink-0 mt-0.5" />
|
|||
|
|
<span>自动生成的目录结构</span>
|
|||
|
|
</li>
|
|||
|
|
<li className="flex items-start gap-2">
|
|||
|
|
<CheckCircle className="w-4 h-4 text-green-500 flex-shrink-0 mt-0.5" />
|
|||
|
|
<span>所有页面的真实截图(iPhone 14 Pro Max尺寸)</span>
|
|||
|
|
</li>
|
|||
|
|
<li className="flex items-start gap-2">
|
|||
|
|
<CheckCircle className="w-4 h-4 text-green-500 flex-shrink-0 mt-0.5" />
|
|||
|
|
<span>每个页面的功能说明与路径</span>
|
|||
|
|
</li>
|
|||
|
|
<li className="flex items-start gap-2">
|
|||
|
|
<CheckCircle className="w-4 h-4 text-green-500 flex-shrink-0 mt-0.5" />
|
|||
|
|
<span>按功能模块分组整理</span>
|
|||
|
|
</li>
|
|||
|
|
</ul>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Note */}
|
|||
|
|
<p className="text-xs text-muted-foreground text-center">生成过程需要30-60秒,请耐心等待</p>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Preview Modal */}
|
|||
|
|
{showPreview && previewPath && (
|
|||
|
|
<div className="fixed inset-0 bg-black/80 z-50 flex items-center justify-center p-4">
|
|||
|
|
<div className="bg-card rounded-2xl w-full max-w-md overflow-hidden border border-border">
|
|||
|
|
<div className="flex items-center justify-between px-4 py-3 border-b border-border">
|
|||
|
|
<h3 className="font-medium text-sm">页面预览</h3>
|
|||
|
|
<div className="flex items-center gap-2">
|
|||
|
|
<button
|
|||
|
|
type="button"
|
|||
|
|
onClick={() => iframeRef.current?.contentWindow?.location.reload()}
|
|||
|
|
className="p-1.5 text-muted-foreground hover:text-foreground rounded-lg"
|
|||
|
|
>
|
|||
|
|
<RefreshCw className="w-4 h-4" />
|
|||
|
|
</button>
|
|||
|
|
<button
|
|||
|
|
type="button"
|
|||
|
|
onClick={() => setShowPreview(false)}
|
|||
|
|
className="p-1.5 text-muted-foreground hover:text-foreground rounded-lg"
|
|||
|
|
>
|
|||
|
|
<XCircle className="w-4 h-4" />
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div className="w-full aspect-[430/932] bg-white">
|
|||
|
|
<iframe ref={iframeRef} src={previewPath} className="w-full h-full" title="Page Preview" />
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</main>
|
|||
|
|
)
|
|||
|
|
}
|