237 lines
9.0 KiB
TypeScript
237 lines
9.0 KiB
TypeScript
import { useState, useEffect, useCallback } from 'react'
|
||
import { Breadcrumb, Button, Card, Table, Modal, Form, Input, InputNumber, message, Space, Popconfirm, Upload, Typography } from 'antd'
|
||
import { FolderOutlined, FileOutlined, PlusOutlined, EditOutlined, DeleteOutlined, UploadOutlined, ArrowLeftOutlined } from '@ant-design/icons'
|
||
import type { ColumnsType } from 'antd/es/table'
|
||
import { listFolders, createFolder, updateFolder, deleteFolder, listFiles, createFile, deleteFile, type DocFolder, type DocFile } from '@/api/document'
|
||
import { getUploadSignature } from '@/api/oss'
|
||
|
||
const ACCEPT_MIME = '.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt'
|
||
/** 可上传文件类型说明(展示用) */
|
||
const UPLOAD_FILE_TIP = '支持上传:PDF、Word(.doc/.docx)、Excel(.xls/.xlsx)、PPT(.ppt/.pptx)、TXT 等格式。'
|
||
|
||
const DocumentList = () => {
|
||
const [folders, setFolders] = useState<DocFolder[]>([])
|
||
const [files, setFiles] = useState<DocFile[]>([])
|
||
const [currentFolder, setCurrentFolder] = useState<DocFolder | null>(null)
|
||
const [loading, setLoading] = useState(false)
|
||
const [folderModalVisible, setFolderModalVisible] = useState(false)
|
||
const [editingFolder, setEditingFolder] = useState<DocFolder | null>(null)
|
||
const [uploading, setUploading] = useState(false)
|
||
const [form] = Form.useForm()
|
||
|
||
const fetchFolders = useCallback(async () => {
|
||
setLoading(true)
|
||
try {
|
||
const res = await listFolders()
|
||
if (res.success && res.list) setFolders(res.list)
|
||
} catch (e) {
|
||
message.error('获取文件夹列表失败')
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}, [])
|
||
|
||
const fetchFiles = useCallback(async (folderId: string) => {
|
||
setLoading(true)
|
||
try {
|
||
const res = await listFiles(folderId)
|
||
if (res.success && res.list) setFiles(res.list)
|
||
} catch (e) {
|
||
message.error('获取文件列表失败')
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}, [])
|
||
|
||
useEffect(() => {
|
||
fetchFolders()
|
||
}, [fetchFolders])
|
||
|
||
useEffect(() => {
|
||
if (currentFolder) fetchFiles(currentFolder.id)
|
||
else setFiles([])
|
||
}, [currentFolder, fetchFiles])
|
||
|
||
const handleCreateFolder = () => {
|
||
setEditingFolder(null)
|
||
form.resetFields()
|
||
setFolderModalVisible(true)
|
||
}
|
||
|
||
const handleEditFolder = (record: DocFolder) => {
|
||
setEditingFolder(record)
|
||
form.setFieldsValue({ name: record.name, sort_order: record.sort_order })
|
||
setFolderModalVisible(true)
|
||
}
|
||
|
||
const handleFolderSubmit = async () => {
|
||
try {
|
||
const values = await form.validateFields()
|
||
if (editingFolder) {
|
||
await updateFolder({ id: editingFolder.id, name: values.name, sort_order: values.sort_order ?? 0 })
|
||
message.success('更新成功')
|
||
} else {
|
||
await createFolder({ name: values.name, sort_order: values.sort_order ?? 0 })
|
||
message.success('创建成功')
|
||
}
|
||
setFolderModalVisible(false)
|
||
fetchFolders()
|
||
} catch (e: any) {
|
||
if (e.errorFields) return
|
||
message.error(e.message || '操作失败')
|
||
}
|
||
}
|
||
|
||
const handleDeleteFolder = async (id: string) => {
|
||
try {
|
||
await deleteFolder(id)
|
||
message.success('删除成功')
|
||
if (currentFolder?.id === id) setCurrentFolder(null)
|
||
fetchFolders()
|
||
} catch (e: any) {
|
||
message.error(e.response?.data?.message || e.message || '删除失败')
|
||
}
|
||
}
|
||
|
||
const handleUploadFile = async (file: File) => {
|
||
if (!currentFolder) return
|
||
setUploading(true)
|
||
try {
|
||
const credentials = await getUploadSignature('doc')
|
||
const key = `${credentials.dir}${Date.now()}_${Math.random().toString(36).slice(2, 8)}.${(file.name.split('.').pop() || '')}`
|
||
const formData = new FormData()
|
||
formData.append('success_action_status', '200')
|
||
formData.append('policy', credentials.policy)
|
||
formData.append('x-oss-signature', credentials.signature)
|
||
formData.append('x-oss-signature-version', credentials.x_oss_signature_version)
|
||
formData.append('x-oss-credential', credentials.x_oss_credential)
|
||
formData.append('x-oss-date', credentials.x_oss_date)
|
||
formData.append('key', key)
|
||
formData.append('x-oss-security-token', credentials.security_token)
|
||
formData.append('file', file)
|
||
const resp = await fetch(credentials.host, { method: 'POST', body: formData })
|
||
if (!resp.ok) throw new Error(`上传失败: ${resp.status}`)
|
||
const fileUrl = `${credentials.host}/${key}`
|
||
await createFile({
|
||
folder_id: currentFolder.id,
|
||
file_name: file.name,
|
||
file_url: fileUrl,
|
||
file_size: file.size,
|
||
mime_type: file.type || '',
|
||
})
|
||
message.success('上传成功')
|
||
fetchFiles(currentFolder.id)
|
||
} catch (e: any) {
|
||
message.error(e.message || '上传失败')
|
||
} finally {
|
||
setUploading(false)
|
||
}
|
||
return false // 阻止 Upload 默认上传
|
||
}
|
||
|
||
const handleDeleteFile = async (id: string) => {
|
||
try {
|
||
await deleteFile(id)
|
||
message.success('删除成功')
|
||
if (currentFolder) fetchFiles(currentFolder.id)
|
||
} catch (e: any) {
|
||
message.error(e.response?.data?.message || e.message || '删除失败')
|
||
}
|
||
}
|
||
|
||
const formatSize = (n: number) => {
|
||
if (n < 1024) return `${n} B`
|
||
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`
|
||
return `${(n / (1024 * 1024)).toFixed(1)} MB`
|
||
}
|
||
|
||
const folderColumns: ColumnsType<DocFolder> = [
|
||
{ title: '名称', dataIndex: 'name', key: 'name', render: (name, record) => <a onClick={() => setCurrentFolder(record)}><FolderOutlined /> {name}</a> },
|
||
{ title: '排序', dataIndex: 'sort_order', key: 'sort_order', width: 80 },
|
||
{
|
||
title: '操作',
|
||
key: 'action',
|
||
width: 140,
|
||
render: (_, record) => (
|
||
<Space>
|
||
<Button type="link" size="small" icon={<EditOutlined />} onClick={() => handleEditFolder(record)}>编辑</Button>
|
||
<Popconfirm title="确定删除该文件夹?其下文件记录将一并删除。" onConfirm={() => handleDeleteFolder(record.id)}>
|
||
<Button type="link" danger size="small" icon={<DeleteOutlined />}>删除</Button>
|
||
</Popconfirm>
|
||
</Space>
|
||
),
|
||
},
|
||
]
|
||
|
||
const fileColumns: ColumnsType<DocFile> = [
|
||
{ title: '名称', dataIndex: 'name', key: 'name', render: (name, row) => <a href={row.file_url} target="_blank" rel="noopener noreferrer"><FileOutlined /> {name}</a> },
|
||
{ title: '原始文件名', dataIndex: 'file_name', key: 'file_name' },
|
||
{ title: '大小', dataIndex: 'file_size', key: 'file_size', width: 100, render: (n: number) => formatSize(n) },
|
||
{ title: '类型', dataIndex: 'mime_type', key: 'mime_type', width: 120 },
|
||
{
|
||
title: '操作',
|
||
key: 'action',
|
||
width: 80,
|
||
render: (_, record) => (
|
||
<Popconfirm title="确定删除该文档记录?" onConfirm={() => handleDeleteFile(record.id)}>
|
||
<Button type="link" danger size="small" icon={<DeleteOutlined />}>删除</Button>
|
||
</Popconfirm>
|
||
),
|
||
},
|
||
]
|
||
|
||
return (
|
||
<div style={{ padding: 24 }}>
|
||
<Breadcrumb style={{ marginBottom: 16 }} items={[
|
||
{ title: <a onClick={() => setCurrentFolder(null)}>文档管理</a> },
|
||
...(currentFolder ? [{ title: currentFolder.name }] : []),
|
||
]} />
|
||
<Card
|
||
title={currentFolder ? `文件夹:${currentFolder.name}` : '文件夹列表'}
|
||
extra={
|
||
<Space>
|
||
{currentFolder ? (
|
||
<>
|
||
<Button icon={<ArrowLeftOutlined />} onClick={() => setCurrentFolder(null)}>返回</Button>
|
||
<Upload accept={ACCEPT_MIME} showUploadList={false} beforeUpload={handleUploadFile} disabled={uploading}>
|
||
<Button type="primary" icon={<UploadOutlined />} loading={uploading}>上传文档</Button>
|
||
</Upload>
|
||
</>
|
||
) : (
|
||
<Button type="primary" icon={<PlusOutlined />} onClick={handleCreateFolder}>新建文件夹</Button>
|
||
)}
|
||
</Space>
|
||
}
|
||
>
|
||
{currentFolder ? (
|
||
<Typography.Text type="secondary" style={{ display: 'block', marginBottom: 12 }}>{UPLOAD_FILE_TIP}</Typography.Text>
|
||
) : null}
|
||
{!currentFolder ? (
|
||
<Table rowKey="id" loading={loading} columns={folderColumns} dataSource={folders} pagination={false} />
|
||
) : (
|
||
<Table rowKey="id" loading={loading} columns={fileColumns} dataSource={files} pagination={false} />
|
||
)}
|
||
</Card>
|
||
|
||
<Modal
|
||
title={editingFolder ? '编辑文件夹' : '新建文件夹'}
|
||
open={folderModalVisible}
|
||
onOk={handleFolderSubmit}
|
||
onCancel={() => setFolderModalVisible(false)}
|
||
destroyOnClose
|
||
>
|
||
<Form form={form} layout="vertical">
|
||
<Form.Item name="name" label="文件夹名称" rules={[{ required: true, message: '请输入名称' }]}>
|
||
<Input placeholder="请输入文件夹名称" />
|
||
</Form.Item>
|
||
<Form.Item name="sort_order" label="排序" initialValue={0}>
|
||
<InputNumber min={0} style={{ width: '100%' }} />
|
||
</Form.Item>
|
||
</Form>
|
||
</Modal>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default DocumentList
|