Update remote soul-content with local content
This commit is contained in:
3
app/documentation/capture/loading.tsx
Normal file
3
app/documentation/capture/loading.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Loading() {
|
||||
return null
|
||||
}
|
||||
78
app/documentation/capture/page.tsx
Normal file
78
app/documentation/capture/page.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
import { useSearchParams } from "next/navigation"
|
||||
|
||||
export default function DocumentationCapturePage() {
|
||||
const searchParams = useSearchParams()
|
||||
const path = searchParams.get("path") || "/"
|
||||
const [loaded, setLoaded] = useState(false)
|
||||
const [timeoutReached, setTimeoutReached] = useState(false)
|
||||
const [loadError, setLoadError] = useState<string | null>(null)
|
||||
|
||||
const src = useMemo(() => {
|
||||
if (!path.startsWith("/")) return `/${path}`
|
||||
return path
|
||||
}, [path])
|
||||
|
||||
useEffect(() => {
|
||||
setLoaded(false)
|
||||
setTimeoutReached(false)
|
||||
setLoadError(null)
|
||||
|
||||
const timer = window.setTimeout(() => {
|
||||
if (!loaded) {
|
||||
setTimeoutReached(true)
|
||||
}
|
||||
}, 60000)
|
||||
|
||||
return () => window.clearTimeout(timer)
|
||||
}, [src, loaded])
|
||||
|
||||
const handleLoad = () => {
|
||||
setLoaded(true)
|
||||
setTimeoutReached(false)
|
||||
}
|
||||
|
||||
const handleError = () => {
|
||||
setLoadError("页面加载失败")
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-white flex items-center justify-center">
|
||||
<div className="w-[430px] h-[932px] border border-gray-200 bg-white relative overflow-hidden">
|
||||
<iframe
|
||||
data-doc-iframe="true"
|
||||
data-loaded={loaded ? "true" : "false"}
|
||||
src={src}
|
||||
className="w-full h-full border-0"
|
||||
onLoad={handleLoad}
|
||||
onError={handleError}
|
||||
title={`Capture: ${path}`}
|
||||
sandbox="allow-scripts allow-same-origin allow-forms allow-popups"
|
||||
/>
|
||||
|
||||
{!loaded && !timeoutReached && !loadError && (
|
||||
<div className="absolute inset-0 bg-white flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="w-8 h-8 border-2 border-gray-300 border-t-gray-600 rounded-full animate-spin mx-auto mb-2" />
|
||||
<p className="text-sm text-gray-500">加载中...</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{(timeoutReached || loadError) && (
|
||||
<div className="fixed left-0 top-0 right-0 bg-red-600 text-white text-sm px-3 py-2 text-center">
|
||||
{loadError || "页面加载超时"}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loaded && (
|
||||
<div className="fixed left-0 bottom-0 right-0 bg-green-600 text-white text-xs px-3 py-1 text-center">
|
||||
页面已加载: {path}
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
)
|
||||
}
|
||||
306
app/documentation/page.tsx
Normal file
306
app/documentation/page.tsx
Normal file
@@ -0,0 +1,306 @@
|
||||
"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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user