Update remote soul-content with local content

This commit is contained in:
卡若
2026-01-09 11:58:08 +08:00
parent 2bdf275cba
commit d781dc07ed
172 changed files with 16577 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
export default function Loading() {
return null
}

View 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
View 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>
)
}