2025-01-31 19:32:50 +08:00

340 lines
8.3 KiB
TypeScript

// app/components/FileTree/FileTree.tsx
'use client'
import { useState, useEffect } from 'react'
import { Tree, Input, Button, Dropdown, Modal, Form, message } from 'antd'
import type { DataNode, TreeProps } from 'antd/es/tree'
import { Key } from 'react'
import {
FileOutlined,
FolderOutlined,
PlusOutlined,
FolderOpenOutlined,
LoadingOutlined,
} from '@ant-design/icons'
import { useRouter } from 'next/navigation'
import { FileService } from '@/app/lib/services/file'
interface FileNode {
id: string
name: string
type: string
children?: FileNode[]
parentKey?: string
}
interface FileModalProps {
type: 'new-doc' | 'new-folder' | 'rename'
title: string
open: boolean
initialValue?: string
onOk: (value: string) => void
onCancel: () => void
loading?: boolean
}
// 文件操作模态框组件
const FileModal: React.FC<FileModalProps> = ({
type,
title,
open,
initialValue = '',
onOk,
onCancel,
loading,
}) => {
const [form] = Form.useForm()
useEffect(() => {
if (open) {
form.setFieldsValue({ name: initialValue })
}
}, [open, initialValue, form])
const handleOk = async () => {
try {
const values = await form.validateFields()
onOk(values.name)
} catch (error) {
// 表单验证错误
}
}
return (
<Modal
title={title}
open={open}
onOk={handleOk}
onCancel={onCancel}
confirmLoading={loading}
>
<Form form={form} layout="vertical">
<Form.Item
name="name"
label="名称"
rules={[
{ required: true, message: '请输入名称' },
{
pattern: /^[^\\/:*?"<>|]+$/,
message: '名称不能包含特殊字符 \\ / : * ? " < > |'
}
]}
>
<Input
autoFocus
placeholder={type === 'new-folder' ? '新建文件夹' : '新建文档'}
/>
</Form.Item>
</Form>
</Modal>
)
}
export default function FileTree() {
const router = useRouter()
const [treeData, setTreeData] = useState<DataNode[]>([])
const [loading, setLoading] = useState(false)
const [searchValue, setSearchValue] = useState('')
const [selectedKeys, setSelectedKeys] = useState<Key[]>([])
const [expandedKeys, setExpandedKeys] = useState<Key[]>([])
// 模态框状态
const [modal, setModal] = useState<{
type: 'new-doc' | 'new-folder' | 'rename'
open: boolean
title: string
parentId?: string
targetId?: string
initialValue?: string
}>({
type: 'new-doc',
open: false,
title: '',
})
// 加载文件树数据
const loadFileTree = async () => {
try {
setLoading(true)
const files = await FileService.getFileTree()
const formattedData = formatFileTree(files)
setTreeData(formattedData)
} catch (error) {
message.error('加载文件失败')
} finally {
setLoading(false)
}
}
useEffect(() => {
loadFileTree()
}, [])
// 格式化文件树数据
const formatFileTree = (files: FileNode[]): DataNode[] => {
return files.map(file => ({
key: file.id,
title: file.name,
isLeaf: file.type === 'file',
children: file.children ? formatFileTree(file.children) : undefined
}))
}
// 处理新建文档
const handleNewDocument = (parentId: string) => {
setModal({
type: 'new-doc',
open: true,
title: '新建文档',
parentId,
})
}
// 处理新建文件夹
const handleNewFolder = (parentId: string) => {
setModal({
type: 'new-folder',
open: true,
title: '新建文件夹',
parentId,
})
}
// 处理重命名
const handleRename = (targetId: string, initialValue: string) => {
setModal({
type: 'rename',
open: true,
title: '重命名',
targetId,
initialValue,
})
}
// 处理删除
const handleDelete = async (id: string) => {
Modal.confirm({
title: '确认删除',
content: '删除后无法恢复,是否继续?',
okText: '确认',
cancelText: '取消',
onOk: async () => {
try {
await FileService.deleteFile(id)
message.success('删除成功')
loadFileTree()
} catch (error) {
message.error('删除失败'+error)
}
}
})
}
// 处理模态框确认
const handleModalOk = async (name: string) => {
try {
setLoading(true)
if (modal.type === 'rename' && modal.targetId) {
await FileService.updateFile({
id: modal.targetId,
name,
})
message.success('重命名成功')
} else if (modal.parentId) {
await FileService.createFile({
name,
type: modal.type === 'new-folder' ? 'folder' : 'file',
parentId: modal.parentId,
})
message.success(modal.type === 'new-folder' ? '文件夹创建成功' : '文档创建成功')
}
loadFileTree()
setModal({ ...modal, open: false })
} catch (error) {
message.error('操作失败' + error)
} finally {
setLoading(false)
}
}
// 处理模态框取消
const handleModalCancel = () => {
setModal({ ...modal, open: false })
}
// 获取右键菜单
const getContextMenu = (node: DataNode) => ({
items: [
{
key: 'new-doc',
label: '新建文档',
icon: <FileOutlined />,
onClick: () => handleNewDocument(node.key as string),
},
{
key: 'new-folder',
label: '新建文件夹',
icon: <FolderOutlined />,
onClick: () => handleNewFolder(node.key as string),
},
{
type: 'divider',
},
{
key: 'rename',
label: '重命名',
onClick: () => handleRename(node.key as string, node.title as string),
},
{
key: 'delete',
label: '删除',
danger: true,
onClick: () => handleDelete(node.key as string),
},
],
})
// 处理树节点选择
const onSelect: TreeProps['onSelect'] = (selectedKeys, info) => {
setSelectedKeys(selectedKeys as string[])
const key = selectedKeys[0]
if (info.node.isLeaf) {
router.push(`/document/${key}`)
}
}
// 处理树节点展开/收起
const onExpand = (expandedKeys: Key[]) => {
setExpandedKeys(expandedKeys)
}
return (
<div className="h-full flex flex-col">
<div className="p-2 flex items-center gap-2 border-b">
<Input
placeholder="搜索文件..."
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
className="flex-1"
/>
<Dropdown
menu={{
items: [
{
key: 'new-doc',
label: '新建文档',
icon: <FileOutlined />,
onClick: () => handleNewDocument('root'),
},
{
key: 'new-folder',
label: '新建文件夹',
icon: <FolderOutlined />,
onClick: () => handleNewFolder('root'),
},
],
}}
>
<Button type="text" icon={<PlusOutlined />} />
</Dropdown>
</div>
<div className="flex-1 overflow-auto p-2">
{loading ? (
<div className="flex items-center justify-center h-full">
<LoadingOutlined className="text-2xl" />
</div>
) : (
<Tree
treeData={treeData}
onSelect={onSelect}
onExpand={onExpand}
expandedKeys={expandedKeys}
selectedKeys={selectedKeys}
showIcon
icon={(node) =>
node.isLeaf ? <FileOutlined /> :
node.expanded ? <FolderOpenOutlined /> : <FolderOutlined />
}
titleRender={(nodeData) => (
<Dropdown
menu={getContextMenu(nodeData)}
trigger={['contextMenu']}
>
<span className="w-full inline-block">{nodeData.title}</span>
</Dropdown>
)}
/>
)}
</div>
<FileModal
type={modal.type}
title={modal.title}
open={modal.open}
initialValue={modal.initialValue}
onOk={handleModalOk}
onCancel={handleModalCancel}
loading={loading}
/>
</div>
)
}