mirror of
https://github.com/Awuqing/BackupX.git
synced 2026-09-05 23:47:33 +08:00
Merge pull request #20 from Awuqing/feat/community-enhancements
fix: directory picker cannot navigate into subdirectories (#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