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
This commit is contained in:
@@ -1,65 +1,129 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { useState, useCallback } from 'react'
|
||||
import { X, Plus, AlertCircle } from 'lucide-react'
|
||||
import { DocTree } from './DocTree'
|
||||
import { DocContent } from './DocContent'
|
||||
import { parseMarkdown, buildFileTree } from '@/lib/parser'
|
||||
import type { DocFile, ParsedDoc } from '@/lib/types'
|
||||
import { config } from '@/config'
|
||||
import { buildFileTree } from '@/lib/parser'
|
||||
import type { DocFile } from '@/lib/types'
|
||||
|
||||
const modules = import.meta.glob('../docs/api/**/*.md', { as: 'raw', eager: true })
|
||||
interface ExternalDoc {
|
||||
name: string
|
||||
path: string
|
||||
relativePath: string
|
||||
content: string
|
||||
}
|
||||
|
||||
const DOCS_FILES: Record<string, string> = Object.fromEntries(
|
||||
Object.entries(modules).map(([path, content]) => {
|
||||
const relativePath = path.replace('../docs/api/', '')
|
||||
const fileContent = typeof content === 'string' ? content : ''
|
||||
return [relativePath, fileContent]
|
||||
})
|
||||
)
|
||||
interface ApiDocViewerProps {
|
||||
onDocsPathChange?: (path: string) => void;
|
||||
}
|
||||
|
||||
export const ApiDocViewer = () => {
|
||||
export const ApiDocViewer = ({ onDocsPathChange }: ApiDocViewerProps) => {
|
||||
const [fileTree, setFileTree] = useState<DocFile[]>([])
|
||||
const [selectedPath, setSelectedPath] = useState<string | undefined>()
|
||||
const [currentDoc, setCurrentDoc] = useState<ParsedDoc | null>(null)
|
||||
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)
|
||||
|
||||
useEffect(() => {
|
||||
const files = Object.keys(DOCS_FILES)
|
||||
const tree = buildFileTree(files, '/')
|
||||
setFileTree(tree)
|
||||
const loadDocsFromPath = async (basePath: string): Promise<boolean> => {
|
||||
if (!basePath) {
|
||||
setErrorMsg('请输入文档路径')
|
||||
return false
|
||||
}
|
||||
|
||||
if (files.length > 0) {
|
||||
setSelectedPath(files[0])
|
||||
if (!window.electronAPI) {
|
||||
setErrorMsg('Electron API 不可用,请使用打包后的应用')
|
||||
return false
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedPath) {
|
||||
const content = DOCS_FILES[selectedPath]
|
||||
if (content) {
|
||||
const parsed = parseMarkdown(content)
|
||||
setCurrentDoc(parsed)
|
||||
|
||||
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)
|
||||
}
|
||||
}, [selectedPath])
|
||||
}
|
||||
|
||||
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 handleReferenceClick = useCallback((ref: string) => {
|
||||
const normalizedRef = ref.replace('.md', '').replace(/^\.\.\//, '')
|
||||
const allFiles = Object.keys(DOCS_FILES)
|
||||
const match = allFiles.find(f => f.replace('.md', '') === normalizedRef)
|
||||
if (match) {
|
||||
setSelectedPath(match)
|
||||
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-screen bg-[#1e1e1e]">
|
||||
<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]">
|
||||
<h2 className="text-sm font-semibold text-gray-200">{config.sidebarTitle}</h2>
|
||||
<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
|
||||
@@ -72,10 +136,64 @@ export const ApiDocViewer = () => {
|
||||
|
||||
<main className="flex-1 overflow-hidden">
|
||||
<DocContent
|
||||
doc={currentDoc}
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user