mirror of
https://github.com/Awuqing/BackupX.git
synced 2026-08-28 19:47:27 +08:00
fix: directory picker cannot navigate into subdirectories (#19)
Root cause: ArcoDesign Tree loadMore callback receives NodeInstance where the key is at node.props.dataRef.key, not node.props.key. The old code passed node.props directly which resulted in undefined key, causing child directory loading to silently fail. Fix: - Access node key via node.props.dataRef?.key ?? node.props._key - Add showLine + blockNode + folder icons for better visual hierarchy - Add path display with copy button in selection modal - Add unmountOnExit to reset state on close Closes #19
This commit is contained in:
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user