fix: folder click shows overview doc, add same-name filter, move + to header
This commit is contained in:
20
src/App.tsx
20
src/App.tsx
@@ -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} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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">
|
|
||||||
{showLine ? '│' : ' '}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
result += isLast ? '└─ ' : '├─ '
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user