fix: folder click shows overview doc, add same-name filter, move + to header

This commit is contained in:
2026-03-18 20:13:30 +08:00
parent c3ecebea89
commit 8a7f9ad6e8
4 changed files with 86 additions and 55 deletions

View File

@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { FileText, Box } from 'lucide-react'; import { FileText, Box, Plus } from 'lucide-react';
import { ApiDocViewer } from './components/ApiDocViewer'; import { ApiDocViewer } from './components/ApiDocViewer';
import BlueprintPage from './components/blueprint/BlueprintPage'; import BlueprintPage from './components/blueprint/BlueprintPage';
import { config } from './config'; import { config } from './config';
@@ -26,6 +26,7 @@ declare global {
function App() { function App() {
const [currentPage, setCurrentPage] = useState<Page>('docs'); const [currentPage, setCurrentPage] = useState<Page>('docs');
const [docsPath, setDocsPath] = useState<string>(''); const [docsPath, setDocsPath] = useState<string>('');
const [showAddModal, setShowAddModal] = useState(false);
useEffect(() => { useEffect(() => {
document.title = config.projectName; document.title = config.projectName;
@@ -41,7 +42,7 @@ function App() {
<nav className="flex gap-1"> <nav className="flex gap-1">
<button <button
onClick={() => setCurrentPage('docs')} onClick={() => setCurrentPage('docs')}
className={`flex items-center gap-2 px-3 py-1.5 rounded text-sm ${ className={`flex items-center gap-2 px-3 py-1 rounded text-sm ${
currentPage === 'docs' currentPage === 'docs'
? 'bg-zinc-800 text-white' ? 'bg-zinc-800 text-white'
: 'text-zinc-400 hover:text-white hover:bg-zinc-800/50' : 'text-zinc-400 hover:text-white hover:bg-zinc-800/50'
@@ -52,7 +53,7 @@ function App() {
</button> </button>
<button <button
onClick={() => setCurrentPage('blueprint')} onClick={() => setCurrentPage('blueprint')}
className={`flex items-center gap-2 px-3 py-1.5 rounded text-sm ${ className={`flex items-center gap-2 px-3 py-1 rounded text-sm ${
currentPage === 'blueprint' currentPage === 'blueprint'
? 'bg-zinc-800 text-white' ? 'bg-zinc-800 text-white'
: 'text-zinc-400 hover:text-white hover:bg-zinc-800/50' : 'text-zinc-400 hover:text-white hover:bg-zinc-800/50'
@@ -62,11 +63,22 @@ function App() {
3D 3D
</button> </button>
</nav> </nav>
<button
onClick={() => setShowAddModal(true)}
className="flex items-center gap-1 px-3 py-1 rounded text-sm text-zinc-400 hover:text-white hover:bg-zinc-800/50"
title="添加文档路径"
>
<Plus size={16} />
</button>
</div> </div>
</header> </header>
<main className="flex-1 overflow-hidden"> <main className="flex-1 overflow-hidden">
{currentPage === 'docs' ? ( {currentPage === 'docs' ? (
<ApiDocViewer onDocsPathChange={setDocsPath} /> <ApiDocViewer
onDocsPathChange={setDocsPath}
showAddModal={showAddModal}
onCloseAddModal={() => setShowAddModal(false)}
/>
) : ( ) : (
<BlueprintPage docsPath={docsPath} /> <BlueprintPage docsPath={docsPath} />
)} )}

View File

@@ -1,5 +1,5 @@
import { useState, useCallback } from 'react' import { useState, useCallback, useEffect } from 'react'
import { X, Plus, AlertCircle } from 'lucide-react' import { X, AlertCircle } from 'lucide-react'
import { DocTree } from './DocTree' import { DocTree } from './DocTree'
import { DocContent } from './DocContent' import { DocContent } from './DocContent'
import { buildFileTree } from '@/lib/parser' import { buildFileTree } from '@/lib/parser'
@@ -14,9 +14,11 @@ interface ExternalDoc {
interface ApiDocViewerProps { interface ApiDocViewerProps {
onDocsPathChange?: (path: string) => void; onDocsPathChange?: (path: string) => void;
showAddModal?: boolean;
onCloseAddModal?: () => void;
} }
export const ApiDocViewer = ({ onDocsPathChange }: ApiDocViewerProps) => { export const ApiDocViewer = ({ onDocsPathChange, showAddModal, onCloseAddModal }: ApiDocViewerProps) => {
const [fileTree, setFileTree] = useState<DocFile[]>([]) const [fileTree, setFileTree] = useState<DocFile[]>([])
const [selectedPath, setSelectedPath] = useState<string | undefined>() const [selectedPath, setSelectedPath] = useState<string | undefined>()
const [currentContent, setCurrentContent] = useState<string>('') const [currentContent, setCurrentContent] = useState<string>('')
@@ -26,6 +28,13 @@ export const ApiDocViewer = ({ onDocsPathChange }: ApiDocViewerProps) => {
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const [errorMsg, setErrorMsg] = useState<string | null>(null) const [errorMsg, setErrorMsg] = useState<string | null>(null)
useEffect(() => {
if (showAddModal) {
setShowModal(true)
setErrorMsg(null)
}
}, [showAddModal])
const loadDocsFromPath = async (basePath: string): Promise<boolean> => { const loadDocsFromPath = async (basePath: string): Promise<boolean> => {
if (!basePath) { if (!basePath) {
setErrorMsg('请输入文档路径') setErrorMsg('请输入文档路径')
@@ -61,7 +70,17 @@ export const ApiDocViewer = ({ onDocsPathChange }: ApiDocViewerProps) => {
} }
setExternalDocs(docs) setExternalDocs(docs)
const fileList = docs.map(d => d.relativePath.replace(/^api\//, ''))
// 过滤掉与父文件夹同名的 md 文件,只用于目录树显示
const filteredDocs = docs.filter(doc => {
const parts = doc.relativePath.split('/')
if (parts.length < 2) return true
const filename = parts[parts.length - 1].replace(/\.md$/, '')
const parentFolder = parts[parts.length - 2]
return filename !== parentFolder
})
const fileList = filteredDocs.map(d => d.relativePath.replace(/^api\//, ''))
const tree = buildFileTree(fileList, '/') const tree = buildFileTree(fileList, '/')
setFileTree(tree) setFileTree(tree)
@@ -79,12 +98,20 @@ export const ApiDocViewer = ({ onDocsPathChange }: ApiDocViewerProps) => {
} }
const handleSelect = useCallback((file: DocFile) => { const handleSelect = useCallback((file: DocFile) => {
if (!file.isDir) {
setSelectedPath(file.relativePath) setSelectedPath(file.relativePath)
const doc = externalDocs.find(d => d.relativePath === file.relativePath) const doc = externalDocs.find(d => d.relativePath === file.relativePath)
if (doc) { if (doc) {
setCurrentContent(doc.content) setCurrentContent(doc.content)
} }
}, [externalDocs])
const handleFolderClick = useCallback((folderPath: string) => {
const sameNamePath = `${folderPath}/${folderPath}.md`
const sameNameDoc = externalDocs.find(d => d.relativePath === sameNamePath)
if (sameNameDoc) {
setSelectedPath(sameNameDoc.relativePath.replace(/^api\//, ''))
setCurrentContent(sameNameDoc.content)
} }
}, [externalDocs]) }, [externalDocs])
@@ -93,6 +120,7 @@ export const ApiDocViewer = ({ onDocsPathChange }: ApiDocViewerProps) => {
const success = await loadDocsFromPath(docsPath.trim()) const success = await loadDocsFromPath(docsPath.trim())
if (success) { if (success) {
setShowModal(false) setShowModal(false)
onCloseAddModal?.()
onDocsPathChange?.(docsPath.trim()) onDocsPathChange?.(docsPath.trim())
} }
} }
@@ -112,24 +140,15 @@ export const ApiDocViewer = ({ onDocsPathChange }: ApiDocViewerProps) => {
return ( return (
<div className="flex h-full bg-[#1e1e1e]"> <div className="flex h-full bg-[#1e1e1e]">
<aside className="w-64 bg-[#252526] border-r border-[#3c3c3c] flex flex-col"> <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"> <div className="p-3 border-b border-[#3c3c3c]">
<h2 className="text-sm font-semibold text-gray-200"></h2> <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>
<div className="flex-1 overflow-hidden"> <div className="flex-1 overflow-hidden">
<DocTree <DocTree
files={fileTree} files={fileTree}
selectedPath={selectedPath} selectedPath={selectedPath}
onSelect={handleSelect} onSelect={handleSelect}
onFolderClick={handleFolderClick}
/> />
</div> </div>
</aside> </aside>

View File

@@ -44,19 +44,19 @@ export const DocContent = ({ content, onReferenceClick }: DocContentProps) => {
) )
} }
return ( return (
<code className="text-zinc-300 font-mono text-base" {...props}> <code className="text-zinc-300 font-mono text-lg" {...props}>
{children} {children}
</code> </code>
) )
}, },
pre: ({ children }) => ( pre: ({ children }) => (
<pre className="bg-zinc-900 rounded-lg p-4 overflow-x-auto my-6 text-base leading-relaxed"> <pre className="bg-zinc-900 rounded-lg p-5 overflow-x-auto my-6 text-lg leading-relaxed">
{children} {children}
</pre> </pre>
), ),
table: ({ children }) => ( table: ({ children }) => (
<div className="overflow-x-auto my-6"> <div className="overflow-x-auto my-6">
<table className="w-full text-base"> <table className="w-full text-lg">
{children} {children}
</table> </table>
</div> </div>
@@ -67,12 +67,12 @@ export const DocContent = ({ content, onReferenceClick }: DocContentProps) => {
</thead> </thead>
), ),
th: ({ children }) => ( th: ({ children }) => (
<th className="text-left py-3 pr-4 font-medium text-zinc-300 whitespace-nowrap"> <th className="text-left py-4 pr-4 font-medium text-zinc-300 whitespace-nowrap">
{children} {children}
</th> </th>
), ),
td: ({ children }) => ( td: ({ children }) => (
<td className="py-3 pr-4 text-zinc-400"> <td className="py-4 pr-4 text-zinc-400">
{children} {children}
</td> </td>
), ),
@@ -82,7 +82,7 @@ export const DocContent = ({ content, onReferenceClick }: DocContentProps) => {
</tr> </tr>
), ),
h1: ({ children }) => ( h1: ({ children }) => (
<h1 className="text-3xl font-bold text-white mt-0 mb-8"> <h1 className="text-4xl font-bold text-white mt-0 mb-8">
{children} {children}
</h1> </h1>
), ),
@@ -102,7 +102,7 @@ export const DocContent = ({ content, onReferenceClick }: DocContentProps) => {
</h4> </h4>
), ),
p: ({ children }) => ( p: ({ children }) => (
<p className="text-base text-zinc-400 leading-relaxed my-4"> <p className="text-lg text-zinc-400 leading-relaxed my-4">
{children} {children}
</p> </p>
), ),

View File

@@ -7,6 +7,7 @@ interface DocTreeProps {
files: DocFile[] files: DocFile[]
selectedPath?: string selectedPath?: string
onSelect: (file: DocFile) => void onSelect: (file: DocFile) => void
onFolderClick?: (folderPath: string) => void
} }
interface TreeNodeProps { interface TreeNodeProps {
@@ -16,6 +17,7 @@ interface TreeNodeProps {
ancestorsLast: boolean[] ancestorsLast: boolean[]
selectedPath?: string selectedPath?: string
onSelect: (file: DocFile) => void onSelect: (file: DocFile) => void
onFolderClick?: (folderPath: string) => void
expandedSet: Set<string> expandedSet: Set<string>
onToggle: (path: string) => void onToggle: (path: string) => void
} }
@@ -27,6 +29,7 @@ const TreeNode = React.memo(({
ancestorsLast, ancestorsLast,
selectedPath, selectedPath,
onSelect, onSelect,
onFolderClick,
expandedSet, expandedSet,
onToggle, onToggle,
}: TreeNodeProps) => { }: TreeNodeProps) => {
@@ -36,32 +39,24 @@ const TreeNode = React.memo(({
const handleClick = () => { const handleClick = () => {
if (isDir) { if (isDir) {
onFolderClick?.(file.relativePath)
onToggle(file.relativePath) onToggle(file.relativePath)
} else { } else {
onSelect(file) onSelect(file)
} }
} }
const renderIndent = () => { const getIndent = (): string => {
const parts: React.ReactNode[] = [] let result = ''
for (let i = 0; i < level; i++) { for (let i = 0; i < level; i++) {
const showLine = !ancestorsLast[i] result += ancestorsLast[i] ? ' ' : '│ '
parts.push( }
<span key={i} className="inline-block w-4 text-center text-zinc-600"> result += isLast ? '└─ ' : '├─ '
{showLine ? '│' : ' '} return result
</span>
)
} }
const connector = isLast ? '└──' : '├──' const getIndentWidth = (): number => {
parts.push( return level * 3 + 3
<span key="connector" className="text-zinc-500">
{connector}
</span>
)
return parts
} }
const showChildren = isDir && isExpanded && file.children const showChildren = isDir && isExpanded && file.children
@@ -71,7 +66,7 @@ const TreeNode = React.memo(({
<div <div
onClick={handleClick} onClick={handleClick}
className={clsx( className={clsx(
'flex items-center py-1 cursor-pointer text-sm select-none rounded', 'flex items-center py-0.5 cursor-pointer text-base select-none rounded',
isDir isDir
? 'text-zinc-400 hover:text-zinc-200' ? 'text-zinc-400 hover:text-zinc-200'
: isSelected : isSelected
@@ -79,11 +74,14 @@ const TreeNode = React.memo(({
: 'text-zinc-400 hover:text-zinc-200' : 'text-zinc-400 hover:text-zinc-200'
)} )}
> >
<span className="font-mono text-xs leading-none"> <span
{renderIndent()} className="text-zinc-500 font-mono whitespace-pre"
style={{ width: `${getIndentWidth()}ch` }}
>
{getIndent()}
</span> </span>
<span className={clsx( <span className={clsx(
'ml-1 truncate', 'truncate',
isDir && 'font-medium' isDir && 'font-medium'
)}> )}>
{getDisplayName(file.name)} {getDisplayName(file.name)}
@@ -99,6 +97,7 @@ const TreeNode = React.memo(({
ancestorsLast={[...ancestorsLast, isLast]} ancestorsLast={[...ancestorsLast, isLast]}
selectedPath={selectedPath} selectedPath={selectedPath}
onSelect={onSelect} onSelect={onSelect}
onFolderClick={onFolderClick}
expandedSet={expandedSet} expandedSet={expandedSet}
onToggle={onToggle} onToggle={onToggle}
/> />
@@ -109,7 +108,7 @@ const TreeNode = React.memo(({
TreeNode.displayName = 'TreeNode' TreeNode.displayName = 'TreeNode'
export const DocTree = ({ files, selectedPath, onSelect }: DocTreeProps) => { export const DocTree = ({ files, selectedPath, onSelect, onFolderClick }: DocTreeProps) => {
const [expandedSet, setExpandedSet] = useState<Set<string>>(() => { const [expandedSet, setExpandedSet] = useState<Set<string>>(() => {
const set = new Set<string>() const set = new Set<string>()
files.forEach(f => { files.forEach(f => {
@@ -141,6 +140,7 @@ export const DocTree = ({ files, selectedPath, onSelect }: DocTreeProps) => {
ancestorsLast={[]} ancestorsLast={[]}
selectedPath={selectedPath} selectedPath={selectedPath}
onSelect={onSelect} onSelect={onSelect}
onFolderClick={onFolderClick}
expandedSet={expandedSet} expandedSet={expandedSet}
onToggle={handleToggle} onToggle={handleToggle}
/> />