v0.2.0: 全面代码质量优化

- ESLint flat config + Prettier + EditorConfig
- Markdown处理器LRU缓存
- Zustand选择器优化减少重渲染
- CodeMirror Compartment主题热切换
- Preview防抖(150ms) + IndexedDB去抖(500ms)
- 组件拆分: App.tsx 310→104行, Sidebar.tsx 244→90行
- 统一错误处理 errorHandler.ts
- ConfirmDialog替代原生confirm
- LoadingSpinner加载状态
- Toast多条堆叠+类型区分
- 可访问性增强(ARIA属性、键盘导航)
- Vitest测试框架(78个测试用例)
- Git hooks(husky + lint-staged)
- 项目文档(README.md, CONTRIBUTING.md)
This commit is contained in:
thzxx
2026-06-03 22:13:32 +08:00
parent 3cecb0f9eb
commit 7a4e2b0a67
72 changed files with 5103 additions and 894 deletions
@@ -0,0 +1,97 @@
import React from 'react'
import { File, Folder, ChevronRight } from '../Icons'
import type { FileNode } from '../../types/file'
interface FileTreeProps {
nodes: FileNode[]
depth: number
expandedDirs: string[]
toggleDir: (path: string) => void
activeTabId: string | null
activeFilePath: string | null
onFileClick: (path: string) => void
}
/** AR-02: 从 Sidebar.tsx 提取的递归文件树组件 */
export const FileTree = React.memo(function FileTree({
nodes,
depth,
expandedDirs,
toggleDir,
activeFilePath,
onFileClick
}: FileTreeProps) {
return (
<>
{nodes.map((node: FileNode) => {
const isExpanded = node.type === 'dir' && expandedDirs.includes(node.path)
const isActive = node.type === 'file' && node.path === activeFilePath
return (
<React.Fragment key={node.path}>
<div
className={`tree-item ${isActive ? 'active' : ''}`}
style={{ paddingLeft: (8 + depth * 16) + 'px' }}
role="treeitem"
aria-expanded={node.type === 'dir' ? isExpanded : undefined}
aria-selected={isActive}
aria-label={node.name}
tabIndex={0}
onClick={() => {
if (node.type === 'dir') {
toggleDir(node.path)
} else {
onFileClick(node.path)
}
}}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
if (node.type === 'dir') {
toggleDir(node.path)
} else {
onFileClick(node.path)
}
}
}}
>
{node.type === 'dir' ? (
<>
<span className={`tree-arrow ${isExpanded ? 'expanded' : ''}`} aria-hidden="true">
<ChevronRight size={10} />
</span>
<span className="tree-icon" aria-hidden="true">
<Folder size={14} />
</span>
</>
) : (
<>
<span style={{ width: '16px', flexShrink: 0 }} />
<span className="tree-icon" aria-hidden="true">
<File size={14} />
</span>
</>
)}
<span className="tree-name">{node.name}</span>
</div>
{isExpanded && node.children && (
<div role="group">
<FileTree
nodes={node.children}
depth={depth + 1}
expandedDirs={expandedDirs}
toggleDir={toggleDir}
activeTabId={null}
activeFilePath={activeFilePath}
onFileClick={onFileClick}
/>
</div>
)}
</React.Fragment>
)
})}
</>
)
})
FileTree.displayName = 'FileTree'