Files
XCSDD/src/components/ApiDocViewer.tsx
ssdfasd d66f5b09e6 feat: migrate to react-markdown for robust markdown rendering
- Replace custom markdown parser with react-markdown + remark-gfm
- Fix document link navigation (./ and ../ references)
- Simplify doc viewing flow (direct markdown content instead of parsed structure)
- Update electron main process to only use api folder for docs
- Add blueprint loading from docs/blueprint.md dynamically
- Fix sidebar file selection path matching
- Update preload scripts for new API structure
2026-03-18 18:50:26 +08:00

200 lines
6.6 KiB
TypeScript

import { useState, useCallback } from 'react'
import { X, Plus, AlertCircle } from 'lucide-react'
import { DocTree } from './DocTree'
import { DocContent } from './DocContent'
import { buildFileTree } from '@/lib/parser'
import type { DocFile } from '@/lib/types'
interface ExternalDoc {
name: string
path: string
relativePath: string
content: string
}
interface ApiDocViewerProps {
onDocsPathChange?: (path: string) => void;
}
export const ApiDocViewer = ({ onDocsPathChange }: ApiDocViewerProps) => {
const [fileTree, setFileTree] = useState<DocFile[]>([])
const [selectedPath, setSelectedPath] = useState<string | undefined>()
const [currentContent, setCurrentContent] = useState<string>('')
const [showModal, setShowModal] = useState(false)
const [docsPath, setDocsPath] = useState('')
const [externalDocs, setExternalDocs] = useState<ExternalDoc[]>([])
const [isLoading, setIsLoading] = useState(false)
const [errorMsg, setErrorMsg] = useState<string | null>(null)
const loadDocsFromPath = async (basePath: string): Promise<boolean> => {
if (!basePath) {
setErrorMsg('请输入文档路径')
return false
}
if (!window.electronAPI) {
setErrorMsg('Electron API 不可用,请使用打包后的应用')
return false
}
setIsLoading(true)
setErrorMsg(null)
try {
const files = await window.electronAPI.listDocsFiles(basePath)
if (files.length === 0) {
setErrorMsg(`路径 "${basePath}/api" 下没有找到 .md 文件`)
setExternalDocs([])
setFileTree([])
setSelectedPath(undefined)
setCurrentContent('')
return false
}
const docs: ExternalDoc[] = []
for (const file of files) {
const content = await window.electronAPI.readDocFile(file.path)
if (content) {
docs.push({ name: file.name, path: file.path, relativePath: file.relativePath, content })
}
}
setExternalDocs(docs)
const fileList = docs.map(d => d.relativePath.replace(/^api\//, ''))
const tree = buildFileTree(fileList, '/')
setFileTree(tree)
if (fileList.length > 0) {
setSelectedPath(fileList[0])
setCurrentContent(docs[0].content)
}
return true
} catch (err) {
setErrorMsg(`加载失败: ${err}`)
return false
} finally {
setIsLoading(false)
}
}
const handleSelect = useCallback((file: DocFile) => {
if (!file.isDir) {
setSelectedPath(file.relativePath)
const doc = externalDocs.find(d => d.relativePath === file.relativePath)
if (doc) {
setCurrentContent(doc.content)
}
}
}, [externalDocs])
const handleAddDocs = async () => {
if (isLoading) return;
const success = await loadDocsFromPath(docsPath.trim())
if (success) {
setShowModal(false)
onDocsPathChange?.(docsPath.trim())
}
}
const handleReferenceClick = useCallback((href: string) => {
const ref = href.replace(/\.md$/, '')
const match = externalDocs.find(d => {
const docPath = d.relativePath.replace(/\.md$/, '')
return docPath === ref || docPath.endsWith('/' + ref.split('/').pop())
})
if (match) {
setSelectedPath(match.relativePath.replace(/^api\//, ''))
setCurrentContent(match.content)
}
}, [externalDocs])
return (
<div className="flex h-full bg-[#1e1e1e]">
<aside className="w-64 bg-[#252526] border-r border-[#3c3c3c] flex flex-col">
<div className="p-3 border-b border-[#3c3c3c] flex items-center justify-between">
<h2 className="text-sm font-semibold text-gray-200"></h2>
<button
onClick={() => {
setShowModal(true)
setErrorMsg(null)
}}
className="p-1 hover:bg-[#3c3c3c] rounded"
title="添加文档路径"
>
<Plus size={16} className="text-gray-400" />
</button>
</div>
<div className="flex-1 overflow-hidden">
<DocTree
files={fileTree}
selectedPath={selectedPath}
onSelect={handleSelect}
/>
</div>
</aside>
<main className="flex-1 overflow-hidden">
<DocContent
content={currentContent}
onReferenceClick={handleReferenceClick}
/>
</main>
{showModal && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-[#252526] rounded-lg border border-[#3c3c3c] w-96 p-4">
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-semibold text-gray-200"></h3>
<button
onClick={() => setShowModal(false)}
className="p-1 hover:bg-[#3c3c3c] rounded"
>
<X size={16} className="text-gray-400" />
</button>
</div>
{errorMsg && (
<div className="mb-4 p-3 bg-red-900/30 border border-red-800 rounded flex items-start gap-2">
<AlertCircle size={16} className="text-red-400 mt-0.5 flex-shrink-0" />
<span className="text-sm text-red-300">{errorMsg}</span>
</div>
)}
<input
type="text"
value={docsPath}
onChange={(e) => {
setDocsPath(e.target.value)
setErrorMsg(null)
}}
placeholder="输入 docs 文件夹路径"
className="w-full px-3 py-2 bg-[#1e1e1e] border border-[#3c3c3c] rounded text-sm text-gray-200 placeholder-gray-500"
onKeyDown={(e) => e.key === 'Enter' && handleAddDocs()}
autoFocus
/>
<div className="text-xs text-gray-500 mb-4">
示例: C:\project\docs ( api/*.md)
</div>
<div className="flex justify-end gap-2">
<button
onClick={() => setShowModal(false)}
className="px-4 py-2 text-sm text-gray-400 hover:text-white"
>
</button>
<button
onClick={handleAddDocs}
disabled={!docsPath.trim() || isLoading}
className="px-4 py-2 text-sm bg-blue-600 hover:bg-blue-700 disabled:bg-blue-800 disabled:cursor-not-allowed rounded text-white"
>
{isLoading ? '加载中...' : '添加'}
</button>
</div>
</div>
</div>
)}
</div>
)
}