Merge pull request #20 from Awuqing/feat/community-enhancements

fix: directory picker cannot navigate into subdirectories (#19)
This commit is contained in:
Wu Qing
2026-03-31 00:37:50 +08:00
committed by GitHub
+38 -24
View File
@@ -1,4 +1,5 @@
import { Button, Input, Modal, Space, Spin, Tree, Typography } from '@arco-design/web-react' import { Button, Input, Message, Modal, Space, Spin, Tree, Typography } from '@arco-design/web-react'
import { IconFolder, IconFile } from '@arco-design/web-react/icon'
import { useCallback, useState } from 'react' import { useCallback, useState } from 'react'
import { listNodeDirectory } from '../../services/nodes' import { listNodeDirectory } from '../../services/nodes'
import type { DirEntry } from '../../types/nodes' import type { DirEntry } from '../../types/nodes'
@@ -14,8 +15,10 @@ interface DirectoryPickerProps {
interface TreeNodeData { interface TreeNodeData {
key: string key: string
title: string title: string
icon?: React.ReactNode
isLeaf: boolean isLeaf: boolean
children?: TreeNodeData[] children?: TreeNodeData[]
loaded?: boolean
} }
function entriesToTreeNodes(entries: DirEntry[], mode: 'directory' | 'file'): TreeNodeData[] { function entriesToTreeNodes(entries: DirEntry[], mode: 'directory' | 'file'): TreeNodeData[] {
@@ -24,8 +27,8 @@ function entriesToTreeNodes(entries: DirEntry[], mode: 'directory' | 'file'): Tr
.map((entry) => ({ .map((entry) => ({
key: entry.path, key: entry.path,
title: entry.name, title: entry.name,
icon: entry.isDir ? <IconFolder /> : <IconFile />,
isLeaf: !entry.isDir, isLeaf: !entry.isDir,
children: entry.isDir ? [] : undefined,
})) }))
} }
@@ -34,16 +37,15 @@ export function DirectoryPicker({ value, onChange, placeholder, mode = 'director
const [treeData, setTreeData] = useState<TreeNodeData[]>([]) const [treeData, setTreeData] = useState<TreeNodeData[]>([])
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [selectedPath, setSelectedPath] = useState('') const [selectedPath, setSelectedPath] = useState('')
const [error, setError] = useState('')
const loadDirectory = useCallback( const loadDirectory = useCallback(
async (path: string) => { async (path: string): Promise<TreeNodeData[]> => {
if (nodeId === undefined) return [] if (nodeId === undefined) return []
try { try {
const entries = await listNodeDirectory(nodeId, path) const entries = await listNodeDirectory(nodeId, path)
return entriesToTreeNodes(entries, mode) return entriesToTreeNodes(entries, mode)
} catch { } catch {
setError('加载目录失败') Message.error(`加载目录失败: ${path}`)
return [] return []
} }
}, },
@@ -53,7 +55,6 @@ export function DirectoryPicker({ value, onChange, placeholder, mode = 'director
async function handleOpen() { async function handleOpen() {
setModalVisible(true) setModalVisible(true)
setSelectedPath(value || '') setSelectedPath(value || '')
setError('')
setLoading(true) setLoading(true)
try { try {
const rootNodes = await loadDirectory('/') const rootNodes = await loadDirectory('/')
@@ -63,21 +64,26 @@ export function DirectoryPicker({ value, onChange, placeholder, mode = 'director
} }
} }
async function handleLoadMore(node: TreeNodeData) { // ArcoDesign Tree loadMore: node.props.dataRef 指向 treeData 中的原始对象
const children = await loadDirectory(node.key) async function handleLoadMore(treeNode: any): Promise<void> {
const nodeKey = treeNode.props.dataRef?.key ?? treeNode.props._key
if (!nodeKey) return
const children = await loadDirectory(nodeKey)
setTreeData((prev) => { setTreeData((prev) => {
function updateChildren(nodes: TreeNodeData[]): TreeNodeData[] { function insertChildren(nodes: TreeNodeData[]): TreeNodeData[] {
return nodes.map((n) => { return nodes.map((n) => {
if (n.key === node.key) { if (n.key === nodeKey) {
return { ...n, children } return { ...n, children, loaded: true }
} }
if (n.children) { if (n.children && n.children.length > 0) {
return { ...n, children: updateChildren(n.children) } return { ...n, children: insertChildren(n.children) }
} }
return n return n
}) })
} }
return updateChildren(prev) return insertChildren(prev)
}) })
} }
@@ -88,6 +94,7 @@ export function DirectoryPicker({ value, onChange, placeholder, mode = 'director
setModalVisible(false) setModalVisible(false)
} }
// 没有 nodeId 时退化为普通输入框
if (nodeId === undefined) { if (nodeId === undefined) {
return <Input value={value} placeholder={placeholder} onChange={onChange} /> return <Input value={value} placeholder={placeholder} onChange={onChange} />
} }
@@ -108,30 +115,37 @@ export function DirectoryPicker({ value, onChange, placeholder, mode = 'director
onOk={handleConfirm} onOk={handleConfirm}
okText="选择" okText="选择"
cancelText="取消" cancelText="取消"
style={{ width: 520 }} style={{ width: 560 }}
okButtonProps={{ disabled: !selectedPath }} okButtonProps={{ disabled: !selectedPath }}
unmountOnExit
> >
{error && <Typography.Text type="error">{error}</Typography.Text>}
{selectedPath && ( {selectedPath && (
<Typography.Text type="secondary" style={{ display: 'block', marginBottom: 8 }}> <div style={{ padding: '8px 12px', marginBottom: 12, background: 'var(--color-fill-2)', borderRadius: 4 }}>
: {selectedPath} <Typography.Text copyable style={{ fontSize: 13 }}>
</Typography.Text> {selectedPath}
</Typography.Text>
</div>
)} )}
{loading ? ( {loading ? (
<Spin style={{ display: 'block', textAlign: 'center', padding: 24 }} /> <Spin style={{ display: 'block', textAlign: 'center', padding: 40 }} />
) : treeData.length === 0 ? ( ) : treeData.length === 0 ? (
<Typography.Text type="secondary"></Typography.Text> <Typography.Text type="secondary" style={{ display: 'block', textAlign: 'center', padding: 40 }}>
</Typography.Text>
) : ( ) : (
<div style={{ maxHeight: 400, overflow: 'auto' }}> <div style={{ maxHeight: 420, overflow: 'auto', border: '1px solid var(--color-border)', borderRadius: 4, padding: '4px 0' }}>
<Tree <Tree
blockNode
showLine
treeData={treeData as any} treeData={treeData as any}
selectedKeys={selectedPath ? [selectedPath] : []}
onSelect={(keys) => { onSelect={(keys) => {
if (keys.length > 0) { if (keys.length > 0) {
setSelectedPath(keys[0] as string) setSelectedPath(keys[0] as string)
} }
}} }}
selectedKeys={selectedPath ? [selectedPath] : []} loadMore={handleLoadMore}
loadMore={(node: any) => handleLoadMore(node.props)} icons={{ switcherIcon: <IconFolder style={{ fontSize: 14 }} /> }}
/> />
</div> </div>
)} )}